diff options
| author | Michael Save <savetheinternet@tinyboard.org> | 2012-04-12 21:56:01 +1000 |
|---|---|---|
| committer | Michael Save <savetheinternet@tinyboard.org> | 2012-04-12 21:56:01 +1000 |
| commit | a564a95ab40eb1c9cc7de142db60aedb3e749176 (patch) | |
| tree | b94f1e2bdb8996ca31b75f6270930636d3f3fa39 | |
| parent | dd0f421015059247464efd5f6d2735c5ee08a400 (diff) | |
Improved Tinyboard anti-bot/spam filter. See large comment in inc/config.php for details.
| -rw-r--r-- | inc/anti-bot.php | 83 | ||||
| -rw-r--r-- | inc/config.php | 26 | ||||
| -rw-r--r-- | inc/functions.php | 8 | ||||
| -rw-r--r-- | inc/template.php | 2 | ||||
| -rw-r--r-- | install.php | 13 | ||||
| -rw-r--r-- | install.sql | 17 | ||||
| -rw-r--r-- | mod.php | 10 | ||||
| -rw-r--r-- | post.php | 43 | ||||
| -rw-r--r-- | 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( '<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('"', '"', $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 @@ <?php // Installation/upgrade file -define('VERSION', 'v0.9.6-dev-1'); +define('VERSION', 'v0.9.6-dev-2'); require 'inc/functions.php'; @@ -174,6 +174,17 @@ if(file_exists($config['has_installed'])) { CHANGE `uri` `uri` VARCHAR( 50 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, CHANGE `title` `title` TINYTEXT CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, CHANGE `subtitle` `subtitle` TINYTEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL") or error(db_error()); + case 'v0.9.6-dev-1': + query("CREATE TABLE IF NOT EXISTS `antispam` ( + `board` varchar(255) NOT NULL, + `thread` int(11) DEFAULT NULL, + `hash` bigint(20) NOT NULL, + `created` int(11) NOT NULL, + `expires` int(11) DEFAULT NULL, + `passed` smallint(6) NOT NULL, + PRIMARY KEY (`hash`), + KEY `board` (`board`,`thread`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8;"); case false: // Update version number file_write($config['has_installed'], VERSION); diff --git a/install.sql b/install.sql index 2879e879..0a798198 100644 --- a/install.sql +++ b/install.sql @@ -228,6 +228,23 @@ CREATE TABLE IF NOT EXISTS `cites` ( KEY `post` (`board`,`post`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; +-- -------------------------------------------------------- + +-- +-- Table structure for table `antispam` +-- + +CREATE TABLE IF NOT EXISTS `antispam` ( + `board` varchar(255) NOT NULL, + `thread` int(11) DEFAULT NULL, + `hash` bigint(20) NOT NULL, + `created` int(11) NOT NULL, + `expires` int(11) DEFAULT NULL, + `passed` smallint(6) NOT NULL, + PRIMARY KEY (`hash`), + KEY `board` (`board`,`thread`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; @@ -9,7 +9,7 @@ require 'inc/mod.php'; 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); @@ -1698,6 +1698,10 @@ if(!$mod) { $query->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]; @@ -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 @@ <form name="post" onsubmit="return dopost(this);" enctype="multipart/form-data" action="{{ config.post_url }}" method="post"> -{{ hiddenInputs([board.uri, id]) }} +{{ antibot.html() }} {% if id %}<input type="hidden" name="thread" value="{{ id }}">{% endif %} -{{ hiddenInputs([board.uri, id]) }} +{{ antibot.html() }} <input type="hidden" name="board" value="{{ board.uri }}"> -{{ hiddenInputs([board.uri, id]) }} +{{ antibot.html() }} {% if mod %}<input type="hidden" name="mod" value="1">{% endif %} <table> {% if not config.field_disable_name or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr> <th> {% trans %}Name{% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} </th> <td> <input type="text" name="name" size="25" maxlength="50" autocomplete="off"> - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} </td> </tr>{% endif %} {% if not config.field_disable_email or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr> <th> {% trans %}Email{% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} </th> <td> <input type="text" name="email" size="25" maxlength="40" autocomplete="off"> - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} </td> </tr>{% endif %} <tr> <th> {% trans %}Subject{% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} </th> <td> <input style="float:left;" type="text" name="subject" size="25" maxlength="100" autocomplete="off"> @@ -39,22 +39,22 @@ <tr> <th> {% trans %}Comment{% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} </th> <td> <textarea name="body" id="body" rows="5" cols="35"></textarea> - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} </td> </tr> {% if config.recaptcha %} <tr> <th> {% trans %}Verification{% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} </th> <td> <script type="text/javascript" src="http://www.google.com/recaptcha/api/challenge?k={{ config.recaptcha_public }}"></script> - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} </td> </tr> {% endif %} @@ -64,18 +64,18 @@ </th> <td> <input type="file" name="file"> - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} </td> </tr> {% if config.enable_embedding %} <tr> <th> {% trans %}Embed{% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} </th> <td> <input type="text" name="embed" size="30" maxlength="120" autocomplete="off"> - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} </td> </tr> {% endif %} @@ -103,18 +103,17 @@ {% if not config.field_disable_password or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr> <th> {% trans %}Password{% endtrans %} - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} </th> <td> <input type="password" name="password" size="12" maxlength="18" autocomplete="off"> <span class="unimportant">{% trans %}(For file deletion.){% endtrans %}</span> - {{ hiddenInputs([board.uri, id]) }} + {{ antibot.html() }} </td> </tr>{% endif %} </table> -{{ hiddenInputs([board.uri, id]) }} -<input type="hidden" name="hash" value="{{ hiddenInputsHash([board.uri, id]) }}"> -{{ hiddenInputs([board.uri, id], true) }} +{{ antibot.html(true) }} +<input type="hidden" name="hash" value="{{ antibot.hash() }}"> </form> <script type="text/javascript">{% raw %} |
