@@ -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; | |||
@@ -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(); | |||
} | |||
@@ -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']) { | |||
@@ -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); | |||
@@ -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 */; |
@@ -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']); | |||
} | |||