Browse Source

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

pull/40/head
Michael Save 12 years ago
parent
commit
a564a95ab4
  1. 83
      inc/anti-bot.php
  2. 26
      inc/config.php
  3. 8
      inc/functions.php
  4. 2
      inc/template.php
  5. 13
      install.php
  6. 17
      install.sql
  7. 10
      mod.php
  8. 43
      post.php
  9. 39
      templates/post_form.html

83
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('"', '&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;
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;
}

26
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',

8
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'])
));

2
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),

13
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);

17
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 */;

10
mod.php

@ -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];

43
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"

39
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 %}

Loading…
Cancel
Save