summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--inc/config.php177
-rw-r--r--inc/filters.php109
-rw-r--r--inc/functions.php58
-rw-r--r--install.php17
-rw-r--r--install.sql21
-rw-r--r--post.php23
6 files changed, 294 insertions, 111 deletions
diff --git a/inc/config.php b/inc/config.php
index e8b65967..81cae233 100644
--- a/inc/config.php
+++ b/inc/config.php
@@ -168,13 +168,6 @@
* ====================
*/
- // Minimum time between between each post by the same IP address.
- $config['flood_time'] = 10;
- // Minimum time between between each post with the exact same content AND same IP address.
- $config['flood_time_ip'] = 120;
- // Same as above but by a different IP address. (Same content, not necessarily same IP address.)
- $config['flood_time_same'] = 30;
-
/*
* To further prevent spam and abuse, you can use DNS blacklists (DNSBL). A DNSBL is a list of IP
* addresses published through the Internet Domain Name Service (DNS) either as a zone file that can be
@@ -283,6 +276,118 @@
$config['recaptcha_public'] = '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f';
$config['recaptcha_private'] = '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_';
+ /*
+ * Custom filters detect certain posts and reject/ban accordingly. They are made up of a condition and an
+ * action (for when ALL conditions are met). As every single post has to be put through each filter,
+ * having hundreds probably isn't ideal as it could slow things down.
+ *
+ * By default, the custom filters array is populated with basic flood prevention conditions. This
+ * includes forcing users to wait at least 5 seconds between posts. To disable (or amend) these flood
+ * prevention settings, you will need to empty the $config['filters'] array first. You can do so by
+ * adding "$config['filters'] = array();" to inc/instance-config.php. Basic flood prevention used to be
+ * controlled solely by config variables such as $config['flood_time'] and $config['flood_time_ip'], and
+ * it still is, as long as you leave the relevant $config['filters'] intact. These old config variables
+ * still exist for backwards-compatability and general convenience.
+ *
+ * Read more: http://tinyboard.org/docs/index.php?p=Config/Filters
+ */
+
+ // Minimum time between between each post by the same IP address.
+ $config['flood_time'] = 10;
+ // Minimum time between between each post with the exact same content AND same IP address.
+ $config['flood_time_ip'] = 120;
+ // Same as above but by a different IP address. (Same content, not necessarily same IP address.)
+ $config['flood_time_same'] = 30;
+
+ // Minimum time between posts by the same IP address (all boards).
+ $config['filters'][] = array(
+ 'condition' => array(
+ 'flood-match' => array('ip'), // Only match IP address
+ 'flood-time' => &$config['flood_time'] // 10 seconds minimum
+ ),
+ 'action' => 'reject',
+ 'message' => &$config['error']['flood']
+ );
+
+ // Minimum time between posts by the same IP address with the same text.
+ $config['filters'][] = array(
+ 'condition' => array(
+ 'flood-match' => array('ip', 'body'), // Match IP address and post body
+ 'flood-time' => &$config['flood_time_ip'] // 2 minutes minimum
+ ),
+ 'action' => 'reject',
+ 'message' => &$config['error']['flood']
+ );
+
+ // Minimum time between posts with the same text. (Same content, but not always the same IP address.)
+ $config['filters'][] = array(
+ 'condition' => array(
+ 'flood-match' => array('body'), // Match IP address and post body
+ 'flood-time' => &$config['flood_time_same'] // 30 seconds minimum
+ ),
+ 'action' => 'reject',
+ 'message' => &$config['error']['flood']
+ );
+
+ // Example: Minimum time between posts with the same file hash.
+ // $config['filters'][] = array(
+ // 'condition' => array(
+ // 'flood-match' => array('file'), // Match file hash
+ // 'flood-time' => 60 * 2 // 2 minutes minimum
+ // ),
+ // 'action' => 'reject',
+ // 'message' => &$config['error']['flood']
+ // );
+
+ // An example of blocking an imaginary known spammer, who keeps posting a reply with the name "surgeon",
+ // ending his posts with "regards, the surgeon" or similar.
+ // $config['filters'][] = array(
+ // 'condition' => array(
+ // 'name' => '/^surgeon$/',
+ // 'body' => '/regards,\s+(the )?surgeon$/i',
+ // 'OP' => false
+ // ),
+ // 'action' => 'reject',
+ // 'message' => 'Go away, spammer.'
+ // );
+
+ // Same as above, but issuing a 3-hour ban instead of just reject the post.
+ // $config['filters'][] = array(
+ // 'condition' => array(
+ // 'name' => '/^surgeon$/',
+ // 'body' => '/regards,\s+(the )?surgeon$/i',
+ // 'OP' => false
+ // ),
+ // 'action' => 'ban',
+ // 'expires' => 60 * 60 * 3, // 3 hours
+ // 'reason' => 'Go away, spammer.'
+ // );
+
+ // PHP 5.3+ (anonymous functions)
+ // There is also a "custom" condition, making the possibilities of this feature pretty much endless.
+ // This is a bad example, because there is already a "name" condition built-in.
+ // $config['filters'][] = array(
+ // 'condition' => array(
+ // 'body' => '/h$/i',
+ // 'OP' => false,
+ // 'custom' => function($post) {
+ // if($post['name'] == 'Anonymous')
+ // return true;
+ // else
+ // return false;
+ // }
+ // ),
+ // 'action' => 'reject'
+ // );
+
+ // Filter flood prevention conditions ("flood-match") depend on a table which contains a cache of recent
+ // posts across all boards. This table is automatically purged of older posts, determining the maximum
+ // "age" by looking at each filter. However, when determining the maximum age, Tinyboard does not look
+ // outside the current board. This means that if you have a special flood condition for a specific board
+ // (contained in a board configuration file) which has a flood-time greater than any of those in the
+ // global configuration, you need to set the following variable to the maximum flood-time condition value.
+ // $config['flood_cache'] = 60 * 60 * 24; // 24 hours
+
/*
* ====================
* Post settings
@@ -401,57 +506,6 @@
// Require users to see the ban page at least once for a ban even if it has since expired.
$config['require_ban_view'] = true;
- /*
- * Custom filters detect certain posts and reject/ban accordingly. They are made up of a
- * condition and an action (for when ALL conditions are met). As every single post has to
- * be put through each filter, having hundreds probably isn’t ideal as it could slow things down.
- *
- * Read more: http://tinyboard.org/docs/index.php?p=Config/Filters
- *
- * This used to be named $config['flood_filters'] (still exists as an alias).
- */
-
- // An example of blocking an imaginary known spammer, who keeps posting a reply with the name "surgeon",
- // ending his posts with "regards, the surgeon" or similar.
- // $config['filters'][] = array(
- // 'condition' => array(
- // 'name' => '/^surgeon$/',
- // 'body' => '/regards,\s+(the )?surgeon$/i',
- // 'OP' => false
- // ),
- // 'action' => 'reject',
- // 'message' => 'Go away, spammer.'
- // );
-
- // Same as above, but issuing a 3-hour ban instead of just reject the post.
- // $config['filters'][] = array(
- // 'condition' => array(
- // 'name' => '/^surgeon$/',
- // 'body' => '/regards,\s+(the )?surgeon$/i',
- // 'OP' => false
- // ),
- // 'action' => 'ban',
- // 'expires' => 60 * 60 * 3, // 3 hours
- // 'reason' => 'Go away, spammer.'
- // );
-
- // PHP 5.3+ (anonymous functions)
- // There is also a "custom" condition, making the possibilities of this feature pretty much endless.
- // This is a bad example, because there is already a "name" condition built-in.
- // $config['filters'][] = array(
- // 'condition' => array(
- // 'body' => '/h$/i',
- // 'OP' => false,
- // 'custom' => function($post) {
- // if($post['name'] == 'Anonymous')
- // return true;
- // else
- // return false;
- // }
- // ),
- // 'action' => 'reject'
- // );
-
/*
* ====================
* Markup settings
@@ -593,10 +647,6 @@
// that as a thumbnail instead of resizing/redrawing.
$config['minimum_copy_resize'] = false;
- // Image hashing function. There's really no reason to change this.
- // sha1_file, md5_file, etc. You can also define your own similar function.
- $config['file_hash'] = 'sha1_file';
-
// Maximum image upload size in bytes.
$config['max_filesize'] = 10 * 1024 * 1024; // 10MB
// Maximum image dimensions.
@@ -1156,7 +1206,8 @@
// Post bypass unoriginal content check on robot-enabled boards
$config['mod']['postunoriginal'] = ADMIN;
// Bypass flood check
- $config['mod']['flood'] = ADMIN;
+ $config['mod']['bypass_filters'] = ADMIN;
+ $config['mod']['flood'] = &$config['mod']['bypass_filters'];
// Raw HTML posting
$config['mod']['rawhtml'] = ADMIN;
diff --git a/inc/filters.php b/inc/filters.php
index 253a86b6..81a33116 100644
--- a/inc/filters.php
+++ b/inc/filters.php
@@ -7,6 +7,7 @@
defined('TINYBOARD') or exit;
class Filter {
+ public $flood_check;
private $condition;
public function __construct(array $arr) {
@@ -22,6 +23,56 @@ class Filter {
if (!is_callable($match))
error('Custom condition for filter is not callable!');
return $match($post);
+ case 'flood-match':
+ if (!is_array($match))
+ error('Filter condition "flood-match" must be an array.');
+
+ // Filter out "flood" table entries which do not match this filter.
+
+ $flood_check_matched = array();
+
+ foreach ($this->flood_check as $flood_post) {
+ foreach ($match as $flood_match_arg) {
+ switch ($flood_match_arg) {
+ case 'ip':
+ if ($flood_post['ip'] != $_SERVER['REMOTE_ADDR'])
+ continue 3;
+ break;
+ case 'body':
+ if ($flood_post['posthash'] != md5($post['body_nomarkup']))
+ continue 3;
+ break;
+ case 'file':
+ if (!isset($post['filehash']))
+ return false;
+ if ($flood_post['filehash'] != $post['filehash'])
+ continue 3;
+ break;
+ case 'board':
+ if ($flood_post['board'] != $post['board'])
+ continue 3;
+ break;
+ case 'isreply':
+ if ($flood_post['isreply'] == $post['op'])
+ continue 3;
+ break;
+ default:
+ error('Invalid filter flood condition: ' . $flood_match_arg);
+ }
+ }
+ $flood_check_matched[] = $flood_post;
+ }
+
+ $this->flood_check = $flood_check_matched;
+
+ return !empty($this->flood_check);
+ case 'flood-time':
+ foreach ($this->flood_check as $flood_post) {
+ if (time() - $flood_post['time'] <= $match) {
+ return true;
+ }
+ }
+ return false;
case 'name':
return preg_match($match, $post['name']);
case 'trip':
@@ -31,7 +82,7 @@ class Filter {
case 'subject':
return preg_match($match, $post['subject']);
case 'body':
- return preg_match($match, $post['body']);
+ return preg_match($match, $post['body_nomarkup']);
case 'filename':
if (!$post['has_file'])
return false;
@@ -126,14 +177,64 @@ class Filter {
}
}
+function purge_flood_table() {
+ global $config;
+
+ // Determine how long we need to keep a cache of posts for flood prevention. Unfortunately, it is not
+ // aware of flood filters in other board configurations. You can solve this problem by settings the
+ // config variable $config['flood_cache'] (seconds).
+
+ if (isset($config['flood_cache'])) {
+ $max_time = &$config['flood_cache'];
+ } else {
+ $max_time = 0;
+ foreach ($config['filters'] as $filter) {
+ if (isset($filter['condition']['flood-time']))
+ $max_time = max($max_time, $filter['condition']['flood-time']);
+ }
+ }
+
+ $time = time() - $max_time;
+
+ query("DELETE FROM ``flood`` WHERE ``time`` < $time") or error(db_error());
+}
+
function do_filters(array $post) {
global $config;
- if (!isset($config['filters']))
+ if (!isset($config['filters']) || empty($config['filters']))
return;
- foreach ($config['filters'] as $arr) {
- $filter = new Filter($arr);
+ foreach ($config['filters'] as $filter) {
+ if (isset($filter['condition']['flood-match'])) {
+ $has_flood = true;
+ break;
+ }
+ }
+
+ purge_flood_table();
+
+ if (isset($has_flood)) {
+ if ($post['has_file']) {
+ $query = prepare("SELECT * FROM ``flood`` WHERE `ip` = :ip OR `posthash` = :posthash OR `filehash` = :filehash");
+ $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
+ $query->bindValue(':posthash', md5($post['body_nomarkup']));
+ $query->bindValue(':filehash', $post['filehash']);
+ } else {
+ $query = prepare("SELECT * FROM ``flood`` WHERE `ip` = :ip OR `posthash` = :posthash");
+ $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
+ $query->bindValue(':posthash', md5($post['body_nomarkup']));
+
+ }
+ $query->execute() or error(db_error($query));
+ $flood_check = $query->fetchAll(PDO::FETCH_ASSOC);
+ } else {
+ $flood_check = false;
+ }
+
+ foreach ($config['filters'] as $filter_array) {
+ $filter = new Filter($filter_array);
+ $filter->flood_check = $flood_check;
if ($filter->check($post))
$filter->action();
}
diff --git a/inc/functions.php b/inc/functions.php
index 9601c0f8..a6963c91 100644
--- a/inc/functions.php
+++ b/inc/functions.php
@@ -572,30 +572,6 @@ function listBoards() {
return $boards;
}
-function checkFlood($post) {
- global $board, $config;
-
- $query = prepare(sprintf("SELECT COUNT(*) FROM ``posts_%s`` WHERE
- (`ip` = :ip AND `time` >= :floodtime)
- OR
- (`ip` = :ip AND :body != '' AND `body_nomarkup` = :body AND `time` >= :floodsameiptime)
- OR
- (:body != '' AND `body_nomarkup` = :body AND `time` >= :floodsametime) LIMIT 1", $board['uri']));
- $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
- $query->bindValue(':body', $post['body']);
- $query->bindValue(':floodtime', time()-$config['flood_time'], PDO::PARAM_INT);
- $query->bindValue(':floodsameiptime', time()-$config['flood_time_ip'], PDO::PARAM_INT);
- $query->bindValue(':floodsametime', time()-$config['flood_time_same'], PDO::PARAM_INT);
- $query->execute() or error(db_error($query));
-
- $flood = (bool) $query->fetchColumn();
-
- if (event('check-flood', $post))
- return true;
-
- return $flood;
-}
-
function until($timestamp) {
$difference = $timestamp - time();
if ($difference < 60) {
@@ -780,6 +756,22 @@ function threadExists($id) {
return false;
}
+function insertFloodPost(array $post) {
+ global $board;
+
+ $query = prepare("INSERT INTO ``flood`` VALUES (NULL, :ip, :board, :time, :posthash, :filehash, :isreply)");
+ $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
+ $query->bindValue(':board', $board['uri']);
+ $query->bindValue(':time', time());
+ $query->bindValue(':posthash', md5($post['body_nomarkup']));
+ if ($post['has_file'])
+ $query->bindValue(':filehash', $post['filehash']);
+ else
+ $query->bindValue(':filehash', null, PDO::PARAM_NULL);
+ $query->bindValue(':isreply', !$post['op']);
+ $query->execute() or error(db_error($query));
+}
+
function post(array $post) {
global $pdo, $board;
$query = prepare(sprintf("INSERT INTO ``posts_%s`` VALUES ( NULL, :thread, :subject, :email, :name, :trip, :capcode, :body, :body_nomarkup, :time, :time, :thumb, :thumbwidth, :thumbheight, :file, :width, :height, :filesize, :filename, :filehash, :password, :ip, :sticky, :locked, 0, :embed)", $board['uri']));
@@ -788,19 +780,19 @@ function post(array $post) {
if (!empty($post['subject'])) {
$query->bindValue(':subject', $post['subject']);
} else {
- $query->bindValue(':subject', NULL, PDO::PARAM_NULL);
+ $query->bindValue(':subject', null, PDO::PARAM_NULL);
}
if (!empty($post['email'])) {
$query->bindValue(':email', $post['email']);
} else {
- $query->bindValue(':email', NULL, PDO::PARAM_NULL);
+ $query->bindValue(':email', null, PDO::PARAM_NULL);
}
if (!empty($post['trip'])) {
$query->bindValue(':trip', $post['trip']);
} else {
- $query->bindValue(':trip', NULL, PDO::PARAM_NULL);
+ $query->bindValue(':trip', null, PDO::PARAM_NULL);
}
$query->bindValue(':name', $post['name']);
@@ -811,27 +803,27 @@ function post(array $post) {
$query->bindValue(':ip', isset($post['ip']) ? $post['ip'] : $_SERVER['REMOTE_ADDR']);
if ($post['op'] && $post['mod'] && isset($post['sticky']) && $post['sticky']) {
- $query->bindValue(':sticky', 1, PDO::PARAM_INT);
+ $query->bindValue(':sticky', true, PDO::PARAM_INT);
} else {
- $query->bindValue(':sticky', 0, PDO::PARAM_INT);
+ $query->bindValue(':sticky', false, PDO::PARAM_INT);
}
if ($post['op'] && $post['mod'] && isset($post['locked']) && $post['locked']) {
- $query->bindValue(':locked', 1, PDO::PARAM_INT);
+ $query->bindValue(':locked', true, PDO::PARAM_INT);
} else {
- $query->bindValue(':locked', 0, PDO::PARAM_INT);
+ $query->bindValue(':locked', false, PDO::PARAM_INT);
}
if ($post['mod'] && isset($post['capcode']) && $post['capcode']) {
$query->bindValue(':capcode', $post['capcode'], PDO::PARAM_INT);
} else {
- $query->bindValue(':capcode', NULL, PDO::PARAM_NULL);
+ $query->bindValue(':capcode', null, PDO::PARAM_NULL);
}
if (!empty($post['embed'])) {
$query->bindValue(':embed', $post['embed']);
} else {
- $query->bindValue(':embed', NULL, PDO::PARAM_NULL);
+ $query->bindValue(':embed', null, PDO::PARAM_NULL);
}
if ($post['op']) {
diff --git a/install.php b/install.php
index e8bdda8b..0011a274 100644
--- a/install.php
+++ b/install.php
@@ -1,7 +1,7 @@
<?php
// Installation/upgrade file
-define('VERSION', 'v0.9.6-dev-18');
+define('VERSION', 'v0.9.6-dev-19');
require 'inc/functions.php';
@@ -392,6 +392,21 @@ if (file_exists($config['has_installed'])) {
query("ALTER TABLE ``ip_notes``
DROP INDEX `ip`,
ADD INDEX `ip_lookup` (`ip`, `time`)") or error(db_error());
+ case 'v0.9.6-dev-18':
+ query("CREATE TABLE IF NOT EXISTS ``flood`` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `ip` varchar(39) CHARACTER SET ascii NOT NULL,
+ `board` varchar(58) CHARACTER SET utf8 NOT NULL,
+ `time` int(11) NOT NULL,
+ `posthash` char(32) NOT NULL,
+ `filehash` char(32) DEFAULT NULL,
+ `isreply` tinyint(1) NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `ip` (`ip`),
+ KEY `posthash` (`posthash`),
+ KEY `filehash` (`filehash`),
+ KEY `time` (`time`)
+ ) ENGINE=MyISAM DEFAULT CHARSET=ascii COLLATE=ascii_bin AUTO_INCREMENT=1 ;") or error(db_error());
case false:
// Update version number
file_write($config['has_installed'], VERSION);
diff --git a/install.sql b/install.sql
index a3c03cb1..3c8cc886 100644
--- a/install.sql
+++ b/install.sql
@@ -245,6 +245,27 @@ CREATE TABLE IF NOT EXISTS `theme_settings` (
KEY `theme` (`theme`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `flood`
+--
+
+CREATE TABLE IF NOT EXISTS `flood` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `ip` varchar(39) CHARACTER SET ascii NOT NULL,
+ `board` varchar(58) CHARACTER SET utf8 NOT NULL,
+ `time` int(11) NOT NULL,
+ `posthash` char(32) NOT NULL,
+ `filehash` char(32) DEFAULT NULL,
+ `isreply` tinyint(1) NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `ip` (`ip`),
+ KEY `posthash` (`posthash`),
+ KEY `filehash` (`filehash`),
+ KEY `time` (`time`)
+) ENGINE=MyISAM DEFAULT CHARSET=ascii COLLATE=ascii_bin AUTO_INCREMENT=1 ;
+
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
diff --git a/post.php b/post.php
index deebe668..2a5f65c6 100644
--- a/post.php
+++ b/post.php
@@ -198,7 +198,7 @@ if (isset($_POST['delete'])) {
}
if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
- require 'inc/mod.php';
+ require 'inc/mod/auth.php';
if (!$mod) {
// Liar. You're not a mod.
error($config['error']['notamod']);
@@ -428,11 +428,6 @@ if (isset($_POST['delete'])) {
wordfilters($post['body']);
- // Check for a flood
- if (!hasPermission($config['mod']['flood'], $board['uri']) && checkFlood($post)) {
- error($config['error']['flood']);
- }
-
$post['body'] = escape_markup_modifiers($post['body']);
if ($mod && isset($post['raw']) && $post['raw']) {
@@ -468,10 +463,8 @@ if (isset($_POST['delete'])) {
}
$post['tracked_cites'] = markup($post['body'], true);
+
- require_once 'inc/filters.php';
-
- do_filters($post);
if ($post['has_file']) {
if (!in_array($post['extension'], $config['allowed_ext']) && !in_array($post['extension'], $config['allowed_ext_files']))
@@ -487,9 +480,17 @@ if (isset($_POST['delete'])) {
if (!is_readable($upload))
error($config['error']['nomove']);
- $post['filehash'] = $config['file_hash']($upload);
+ $post['filehash'] = md5_file($upload);
$post['filesize'] = filesize($upload);
+ }
+
+ if (!hasPermission($config['mod']['bypass_filters'], $board['uri'])) {
+ require_once 'inc/filters.php';
+ do_filters($post);
+ }
+
+ if ($post['has_file']) {
if ($is_an_image && $config['ie_mime_type_detection'] !== false) {
// Check IE MIME type detection XSS exploit
$buffer = file_get_contents($upload, null, null, null, 255);
@@ -679,6 +680,8 @@ if (isset($_POST['delete'])) {
$post['id'] = $id = post($post);
+ insertFloodPost($post);
+
if (isset($post['antispam_hash'])) {
incrementSpamHash($post['antispam_hash']);
}