Improved Tinyboard anti-bot/spam filter. See large comment in inc/config.php for details.

This commit is contained in:
Michael Save
2012-04-12 21:56:01 +10:00
parent dd0f421015
commit a564a95ab4
9 changed files with 171 additions and 76 deletions

View File

@@ -12,8 +12,7 @@ if(realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) {
$hidden_inputs_twig = array();
class AntiBot {
public $inputs = array(), $index = 0;
private $salt;
public $salt, $inputs = array(), $index = 0;
public static function randomString($length, $uppercase = false, $special_chars = false) {
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
@@ -90,12 +89,14 @@ class AntiBot {
$this->inputs[$name] = (string)rand(0, 100);
} else {
// Obscure value
$this->inputs[$name] = $this->randomString(rand(5, 100));
$this->inputs[$name] = $this->randomString(rand(5, 100), true, true);
}
}
}
public function html($count = false) {
global $config;
$elements = array(
'<input type="hidden" name="%name%" value="%value%">',
'<input type="hidden" value="%value%" name="%name%">',
@@ -110,7 +111,11 @@ class AntiBot {
$html = '';
if($count == 0) {
if($count === false) {
$count = rand(1, count($this->inputs) / 15);
}
if($count === true) {
// all elements
$inputs = array_slice($this->inputs, $this->index);
} else {
@@ -134,7 +139,10 @@ class AntiBot {
$value = $this->make_confusing($value);
else
$value = utf8tohtml($value);
if(strpos($element, 'textarea') === false)
$value = str_replace('"', '&quot;', $value);
$element = str_replace('%value%', $value, $element);
$html .= $element;
@@ -162,36 +170,42 @@ class AntiBot {
// Use SHA1 for the hash
return sha1($hash . $this->salt);
}
};;
function hiddenInputs(array $salt, $print_the_rest = false) {
global $hidden_inputs_twig;
$salt_str = implode(':', $salt);
if(!isset($hidden_inputs_twig[$salt_str]))
$hidden_inputs_twig[$salt_str] = new AntiBot($salt);
if($print_the_rest)
return $hidden_inputs_twig[$salt_str]->html(0);
else
return $hidden_inputs_twig[$salt_str]->html(rand(1, 5));
}
function hiddenInputsHash(array $salt) {
global $hidden_inputs_twig;
function _create_antibot($board, $thread) {
global $config;
$salt_str = implode(':', $salt);
$antibot = new AntiBot(array($board, $thread));
if(!isset($hidden_inputs_twig[$salt_str]))
$hidden_inputs_twig[$salt_str] = new AntiBot($salt);
query('DELETE FROM `antispam` WHERE `expires` < UNIX_TIMESTAMP()') or error(db_error($query));
return $hidden_inputs_twig[$salt_str]->hash();
if($thread)
$query = prepare('UPDATE `antispam` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` = :thread');
else
$query = prepare('UPDATE `antispam` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` IS NULL');
$query->bindValue(':board', $board);
if($thread)
$query->bindValue(':thread', $thread);
$query->bindValue(':expires', $config['spam']['hidden_inputs_expire']);
$query->execute() or error(db_error($query));
$query = prepare('INSERT INTO `antispam` VALUES (:board, :thread, CRC32(:hash), UNIX_TIMESTAMP(), NULL, 0)');
$query->bindValue(':board', $board);
$query->bindValue(':thread', $thread);
$query->bindValue(':hash', $antibot->hash());
$query->execute() or error(db_error($query));
if($query->rowCount() == 0) {
// there was no database entry for this hash. most likely expired.
return true;
}
return $antibot;
}
function checkSpam(array $extra_salt = array()) {
global $config;
global $config, $pdo;
if(!isset($_POST['hash']))
return true;
@@ -231,6 +245,25 @@ function checkSpam(array $extra_salt = array()) {
// Use SHA1 for the hash
$_hash = sha1($_hash . $extra_salt);
return $hash != $_hash;
if($hash != $_hash)
return true;
$query = prepare('UPDATE `antispam` SET `passed` = `passed` + 1 WHERE `hash` = CRC32(:hash)');
$query->bindValue(':hash', $hash);
$query->execute() or error(db_error($query));
if($query->rowCount() == 0) {
// there was no database entry for this hash. most likely expired.
return true;
}
$query = prepare('SELECT `passed` FROM `antispam` WHERE `hash` = CRC32(:hash)');
$query->bindValue(':hash', $hash);
$query->execute() or error(db_error($query));
$passed = $query->fetchColumn(0);
if($passed > $config['spam']['hidden_inputs_max_pass'])
return true;
return false;
}

View File

@@ -170,9 +170,33 @@
// Skip checking certain IP addresses against blacklists (for troubleshooting or whatever)
$config['dnsbl_exceptions'][] = '127.0.0.1';
// Spam filter
/*
* Introduction to Tinyboard's spam filter:
*
* In simple terms, whenever a posting form on a page is generated (which happens whenever a
* post is made), Tinyboard will add a random amount of hidden, obscure fields to it to
* confuse bots and upset hackers. These fields and their respective obscure values are
* validated upon posting with a 160-bit "hash". That hash can only be used as many times
* as you specify; otherwise, flooding bots could just keep reusing the same hash.
* Once a new set of inputs (and the hash) are generated, old hashes for the same thread
* and board are set to expire. Because you have to reload the page to get the new set
* of inputs and hash, if they expire too quickly and more than one person is viewing the
* page at a given time, Tinyboard would return false positives (depending on how long the
* user sits on the page before posting). If your imageboard is quite fast/popular, set
* $config['spam']['hidden_inputs_max_pass'] and $config['spam']['hidden_inputs_expire'] to
* something higher to avoid false positives.
*
* See also: http://tinyboard.org/docs/?p=Your_request_looks_automated
*
*/
// Number of hidden fields to generate
$config['spam']['hidden_inputs_min'] = 4;
$config['spam']['hidden_inputs_max'] = 12;
// How many times can a "hash" be used to post?
$config['spam']['hidden_inputs_max_pass'] = 30;
// How soon after regeneration do hashes expire (in seconds)?
$config['spam']['hidden_inputs_expire'] = 60 * 60 * 2; // two hours
// These are fields used to confuse the bots. Make sure they aren't actually used by Tinyboard, or it won't work.
$config['spam']['hidden_input_names'] = array(
'user',

View File

@@ -13,7 +13,6 @@ require_once 'inc/display.php';
require_once 'inc/template.php';
require_once 'inc/database.php';
require_once 'inc/events.php';
require_once 'inc/anti-bot.php';
require_once 'inc/lib/gettext/gettext.inc';
// the user is not currently logged in as a moderator
@@ -210,6 +209,11 @@ function _syslog($priority, $message) {
}
}
function create_antibot($board, $thread = null) {
require_once 'inc/anti-bot.php';
return _create_antibot($board, $thread);
}
function rebuildThemes($action) {
// List themes
@@ -1175,6 +1179,7 @@ function buildIndex() {
$content['pages'] = $pages;
$content['pages'][$page-1]['selected'] = true;
$content['btn'] = getPageButtons($content['pages']);
$content['antibot'] = create_antibot($board['uri']);
file_write($filename, Element('index.html', $content));
if(isset($md5) && $md5 == md5_file($filename)) {
@@ -1492,6 +1497,7 @@ function buildThread($id, $return=false, $mod=false) {
'config' => $config,
'id' => $id,
'mod' => $mod,
'antibot' => $mod ? false : create_antibot($board['uri'], $id),
'boardlist' => createBoardlist($mod),
'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['uri'] . '/' . $config['file_index'])
));

View File

@@ -24,7 +24,7 @@ function load_twig() {
$loader = new Twig_Loader_Filesystem($config['dir']['template']);
$loader->setPaths($config['dir']['template']);
$twig = new Twig_Environment($loader, Array(
$twig = new Twig_Environment($loader, array(
'autoescape' => false,
'cache' => "{$config['dir']['template']}/cache",
'debug' => ($config['debug'] ? true : false),