summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--inc/anti-bot.php83
-rw-r--r--inc/config.php26
-rw-r--r--inc/functions.php8
-rw-r--r--inc/template.php2
-rw-r--r--install.php13
-rw-r--r--install.sql17
-rw-r--r--mod.php10
-rw-r--r--post.php43
-rw-r--r--templates/post_form.html39
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('"', '&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;
}
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 */;
diff --git a/mod.php b/mod.php
index 38e423eb..d37089e1 100644
--- a/mod.php
+++ b/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];
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 @@
<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 %}