From a564a95ab40eb1c9cc7de142db60aedb3e749176 Mon Sep 17 00:00:00 2001 From: Michael Save Date: Thu, 12 Apr 2012 21:56:01 +1000 Subject: [PATCH] Improved Tinyboard anti-bot/spam filter. See large comment in inc/config.php for details. --- inc/anti-bot.php | 83 ++++++++++++++++++++++++++++------------ inc/config.php | 26 ++++++++++++- inc/functions.php | 8 +++- inc/template.php | 2 +- install.php | 13 ++++++- install.sql | 17 ++++++++ mod.php | 10 +++-- post.php | 43 +++++++++++---------- templates/post_form.html | 39 +++++++++---------- 9 files changed, 168 insertions(+), 73 deletions(-) diff --git a/inc/anti-bot.php b/inc/anti-bot.php index e8c26113..cd2ff795 100644 --- a/inc/anti-bot.php +++ b/inc/anti-bot.php @@ -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( '', '', @@ -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('"', '"', $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; +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)); - if($print_the_rest) - return $hidden_inputs_twig[$salt_str]->html(0); + if($thread) + $query = prepare('UPDATE `antispam` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` = :thread'); else - return $hidden_inputs_twig[$salt_str]->html(rand(1, 5)); -} - -function hiddenInputsHash(array $salt) { - global $hidden_inputs_twig; + $query = prepare('UPDATE `antispam` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` IS NULL'); - $salt_str = implode(':', $salt); + $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)); - if(!isset($hidden_inputs_twig[$salt_str])) - $hidden_inputs_twig[$salt_str] = new AntiBot($salt); + $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)); - return $hidden_inputs_twig[$salt_str]->hash(); + 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; } diff --git a/inc/config.php b/inc/config.php index 94b7bdd7..b7e3a243 100644 --- a/inc/config.php +++ b/inc/config.php @@ -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', diff --git a/inc/functions.php b/inc/functions.php index 3761b608..da58675c 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -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']) )); diff --git a/inc/template.php b/inc/template.php index b348f153..fc9f167d 100644 --- a/inc/template.php +++ b/inc/template.php @@ -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), diff --git a/install.php b/install.php index 3cd8a5b7..f67d4cb0 100644 --- a/install.php +++ b/install.php @@ -1,7 +1,7 @@ bindValue(':board', $board['uri']); $query->execute() or error(db_error($query)); + $query = prepare("DELETE FROM `antispam` WHERE `board` = :board"); + $query->bindValue(':board', $board['uri']); + $query->execute() or error(db_error($query)); + $_board = $board; rebuildThemes('boards'); @@ -2209,7 +2213,6 @@ if(!$mod) { $page['pages'][$page_no-1]['selected'] = true; $page['btn'] = getPageButtons($page['pages'], true); $page['mod'] = true; - echo Element('index.html', $page); } elseif(preg_match('/^\/' . $regex['board'] . $regex['res'] . $regex['page'] . '$/', $query, $matches)) { // View thread @@ -2354,7 +2357,8 @@ if(!$mod) { if(!openBoard($boardName)) error($config['error']['noboard']); - if(!hasPermission($config['mod']['delete'], $boardName)) error($config['error']['noaccess']); + if(!hasPermission($config['mod']['delete'], $boardName)) + error($config['error']['noaccess']); $post = &$matches[2]; diff --git a/post.php b/post.php index 9b983842..32b0f59f 100644 --- a/post.php +++ b/post.php @@ -5,11 +5,12 @@ */ require 'inc/functions.php'; +require 'inc/anti-bot.php'; // Fix for magic quotes if (get_magic_quotes_gpc()) { function strip_array($var) { - return is_array($var) ? array_map("strip_array", $var) : stripslashes($var); + return is_array($var) ? array_map('strip_array', $var) : stripslashes($var); } $_GET = strip_array($_GET); @@ -192,7 +193,26 @@ if(isset($_POST['delete'])) { } } - if(checkSpam(array($board['uri'], isset($post['thread']) && !($config['quick_reply'] && isset($_POST['quick-reply'])) ? $post['thread'] : null))) + if($post['mod'] = isset($_POST['mod']) && $_POST['mod']) { + require 'inc/mod.php'; + if(!$mod) { + // Liar. You're not a mod. + error($config['error']['notamod']); + } + + $post['sticky'] = $post['op'] && isset($_POST['sticky']); + $post['locked'] = $post['op'] && isset($_POST['lock']); + $post['raw'] = isset($_POST['raw']); + + if($post['sticky'] && !hasPermission($config['mod']['sticky'], $board['uri'])) + error($config['error']['noaccess']); + if($post['locked'] && !hasPermission($config['mod']['lock'], $board['uri'])) + error($config['error']['noaccess']); + if($post['raw'] && !hasPermission($config['mod']['rawhtml'], $board['uri'])) + error($config['error']['noaccess']); + } + + if(!$post['mod'] && checkSpam(array($board['uri'], isset($post['thread']) && !($config['quick_reply'] && isset($_POST['quick-reply'])) ? $post['thread'] : null))) error($config['error']['spam']); if($config['robot_enable'] && $config['robot_mute']) { @@ -239,25 +259,6 @@ if(isset($_POST['delete'])) { } } - if($post['mod'] = isset($_POST['mod']) && $_POST['mod']) { - require 'inc/mod.php'; - if(!$mod) { - // Liar. You're not a mod. - error($config['error']['notamod']); - } - - $post['sticky'] = $post['op'] && isset($_POST['sticky']); - $post['locked'] = $post['op'] && isset($_POST['lock']); - $post['raw'] = isset($_POST['raw']); - - if($post['sticky'] && !hasPermission($config['mod']['sticky'], $board['uri'])) - error($config['error']['noaccess']); - if($post['locked'] && !hasPermission($config['mod']['lock'], $board['uri'])) - error($config['error']['noaccess']); - if($post['raw'] && !hasPermission($config['mod']['rawhtml'], $board['uri'])) - error($config['error']['noaccess']); - } - if(!hasPermission($config['mod']['bypass_field_disable'], $board['uri'])) { if($config['field_disable_name']) $_POST['name'] = $config['anonymous']; // "forced anonymous" diff --git a/templates/post_form.html b/templates/post_form.html index 69b179fd..aab031ed 100644 --- a/templates/post_form.html +++ b/templates/post_form.html @@ -1,35 +1,35 @@
-{{ hiddenInputs([board.uri, id]) }} +{{ antibot.html() }} {% if id %}{% endif %} -{{ hiddenInputs([board.uri, id]) }} +{{ antibot.html() }} -{{ hiddenInputs([board.uri, id]) }} +{{ antibot.html() }} {% if mod %}{% endif %} {% if not config.field_disable_name or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}{% endif %} {% if not config.field_disable_email or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}{% endif %} {% if config.recaptcha %} {% endif %} @@ -64,18 +64,18 @@ {% if config.enable_embedding %} {% endif %} @@ -103,18 +103,17 @@ {% if not config.field_disable_password or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}{% endif %}
{% trans %}Name{% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }}
{% trans %}Email{% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }}
{% trans %}Subject{% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} @@ -39,22 +39,22 @@
{% trans %}Comment{% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }}
{% trans %}Verification{% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }}
- {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }}
{% trans %}Embed{% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }}
{% trans %}Password{% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} {% trans %}(For file deletion.){% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }}
-{{ hiddenInputs([board.uri, id]) }} - -{{ hiddenInputs([board.uri, id], true) }} +{{ antibot.html(true) }} +