From f9d43d20a3660f61439ff4263fd02c39ee4d9975 Mon Sep 17 00:00:00 2001 From: Stephan Brunker Date: Fri, 18 Oct 2019 12:14:10 +0200 Subject: [PATCH] added timeout protection against spambots into event_spamblock --- .../UTF-8/lang_de.inc.php | 8 + .../lang_de.inc.php | 8 + .../lang_en.inc.php | 9 +- .../serendipity_event_spamblock.php | 159 +++++++++++++----- 4 files changed, 138 insertions(+), 46 deletions(-) diff --git a/plugins/serendipity_event_spamblock/UTF-8/lang_de.inc.php b/plugins/serendipity_event_spamblock/UTF-8/lang_de.inc.php index cb0dc155..185b0759 100644 --- a/plugins/serendipity_event_spamblock/UTF-8/lang_de.inc.php +++ b/plugins/serendipity_event_spamblock/UTF-8/lang_de.inc.php @@ -141,3 +141,11 @@ @define('PLUGIN_EVENT_SPAMBLOCK_TRACKBACKIPVALIDATION_URL_EXCLUDE', 'URLs von IP Validatierung ausnehmen'); @define('PLUGIN_EVENT_SPAMBLOCK_TRACKBACKIPVALIDATION_URL_EXCLUDE_DESC', 'URLs, die von der IP Validatierung ausgeschlossen werden sollen. ' . PLUGIN_EVENT_SPAMBLOCK_FILTER_URLS_DESC); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT', 'Wartezeit zum Kommentieren'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_DESC', 'Festsetzen einer Wartezeit zwischen erstem Anzeigen des Artikels und abgesandtem Kommentar'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_TYPE', 'Art der Wartezeit'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_TYPE_FIX', 'festgesetzte Zeit'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_TYPE_ADAPTIVE', 'abhängig von der Länge von Artikel und Kommentar'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_VALUE', 'Wert für Wartezeit'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_VALUE_DESC', 'festgesetzt: Wartezeit in Sekunden / abhängig: Lesen in Wörtern pro Minute und Schreiben in Zeichen pro Minute. 500 ist hier ein guter Wert selbst für schnelle Leser'); +@define('PLUGIN_EVENT_SPAMBLOCK_REASON_TIMEOUT','Entschuldigung, aber du bist kein Mensch und solltest wenigstens versuchen den Artikel zu lesen bevor ein Kommentar verfasst wird'); diff --git a/plugins/serendipity_event_spamblock/lang_de.inc.php b/plugins/serendipity_event_spamblock/lang_de.inc.php index 88844b5c..0a1061d3 100644 --- a/plugins/serendipity_event_spamblock/lang_de.inc.php +++ b/plugins/serendipity_event_spamblock/lang_de.inc.php @@ -141,3 +141,11 @@ @define('PLUGIN_EVENT_SPAMBLOCK_TRACKBACKIPVALIDATION_URL_EXCLUDE', 'URLs von IP Validatierung ausnehmen'); @define('PLUGIN_EVENT_SPAMBLOCK_TRACKBACKIPVALIDATION_URL_EXCLUDE_DESC', 'URLs, die von der IP Validatierung ausgeschlossen werden sollen. ' . PLUGIN_EVENT_SPAMBLOCK_FILTER_URLS_DESC); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT', 'Wartezeit zum Kommentieren'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_DESC', 'Festsetzen einer Wartezeit zwischen erstem Anzeigen des Artikels und abgesandtem Kommentar'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_TYPE', 'Art der Wartezeit'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_TYPE_FIX', 'festgesetzte Zeit'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_TYPE_ADAPTIVE', 'abhängig von der Länge von Artikel und Kommentar'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_VALUE', 'Wert für Wartezeit'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_VALUE_DESC', 'festgesetzt: Wartezeit in Sekunden / abhängig: Lesen in Wörtern pro Minute und Schreiben in Zeichen pro Minute. 500 ist hier ein guter Wert selbst für schnelle Leser'); +@define('PLUGIN_EVENT_SPAMBLOCK_REASON_TIMEOUT','Entschuldigung, aber du bist kein Mensch und solltest wenigstens versuchen den Artikel zu lesen bevor ein Kommentar verfasst wird'); diff --git a/plugins/serendipity_event_spamblock/lang_en.inc.php b/plugins/serendipity_event_spamblock/lang_en.inc.php index ce836108..b27212bc 100644 --- a/plugins/serendipity_event_spamblock/lang_en.inc.php +++ b/plugins/serendipity_event_spamblock/lang_en.inc.php @@ -147,4 +147,11 @@ @define('PLUGIN_EVENT_SPAMBLOCK_SPAM', 'Spam'); @define('PLUGIN_EVENT_SPAMBLOCK_NOT_SPAM', 'Not spam'); -@define('PLUGIN_EVENT_SPAMBLOCK_LOGFILE_VALIDATE', 'Only file extensions .log and .txt are allowed'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT', 'Timeout for commenting'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_DESC', 'Activate a timeout between displaying the article and accepting a comment'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_TYPE', 'Timeout type'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_TYPE_FIX', 'fixed amout of time'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_TYPE_ADAPTIVE', 'depending on the lengths of comment and article'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_VALUE', 'Value for timeout'); +@define('PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_VALUE_DESC', 'type fixed: timeout in seconds / type adaptive: reading in words per minute and typing in chars per minute, 500 is a good value even for fast readers'); +@define('PLUGIN_EVENT_SPAMBLOCK_REASON_TIMEOUT','Sorry, but you are not a human or you should at least attempt to read the article before commenting'); diff --git a/plugins/serendipity_event_spamblock/serendipity_event_spamblock.php b/plugins/serendipity_event_spamblock/serendipity_event_spamblock.php index dc8989ac..957db00b 100644 --- a/plugins/serendipity_event_spamblock/serendipity_event_spamblock.php +++ b/plugins/serendipity_event_spamblock/serendipity_event_spamblock.php @@ -25,7 +25,7 @@ class serendipity_event_spamblock extends serendipity_event 'smarty' => '2.6.7', 'php' => '4.1.0' )); - $propbag->add('version', '1.88.2'); + $propbag->add('version', '1.89'); $propbag->add('event_hooks', array( 'frontend_saveComment' => true, 'external_plugin' => true, @@ -34,6 +34,7 @@ class serendipity_event_spamblock extends serendipity_event 'backend_comments_top' => true, 'backend_view_comment' => true, 'backend_sidebar_admin_appearance' => true, + 'entry_display' => true )); $propbag->add('configuration', array( 'killswitch', @@ -67,6 +68,9 @@ class serendipity_event_spamblock extends serendipity_event 'hide_email', 'checkmail', 'required_fields', + 'comment_timeout', + 'timeout_type', + 'timeout_value', 'automagic_htaccess', 'logtype', 'logfile')); @@ -353,8 +357,6 @@ class serendipity_event_spamblock extends serendipity_event $propbag->add('name', PLUGIN_EVENT_SPAMBLOCK_LOGFILE); $propbag->add('description', PLUGIN_EVENT_SPAMBLOCK_LOGFILE_DESC); $propbag->add('default', $serendipity['serendipityPath'] . 'spamblock-%Y-%m-%d.log'); - $propbag->add('validate', '@\.(log|txt)$@imsU'); - $propbag->add('validate_error', PLUGIN_EVENT_SPAMBLOCK_LOGFILE_VALIDATE); break; case 'logtype': @@ -450,6 +452,31 @@ class serendipity_event_spamblock extends serendipity_event $propbag->add('default', '13'); break; + case 'comment_timeout': + $propbag->add('type', 'boolean'); + $propbag->add('name', PLUGIN_EVENT_SPAMBLOCK_TIMEOUT); + $propbag->add('description', PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_DESC); + $propbag->add('default', false); + break; + + case 'timeout_type': + $propbag->add('type', 'radio'); + $propbag->add('name', PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_TYPE); + $propbag->add('description', ''); + $propbag->add('default', 'fix'); + $propbag->add('radio', array( + 'value' => array('fix', 'adaptive'), + 'desc' => array(PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_TYPE_FIX, PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_TYPE_ADAPTIVE) + )); + break; + + case 'timeout_value': + $propbag->add('type', 'string'); + $propbag->add('name', PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_VALUE); + $propbag->add('description', PLUGIN_EVENT_SPAMBLOCK_TIMEOUT_VALUE_DESC); + $propbag->add('default', '30'); + break; + default: return false; } @@ -840,13 +867,16 @@ class serendipity_event_spamblock extends serendipity_event } $moderation_auto = $this->get_config('moderation_auto', false); - $forcemoderation = $this->get_config('forcemoderation', 60); + $forcemoderation = intval($this->get_config('forcemoderation', 60)); $forcemoderation_treat = $this->get_config('forcemoderation_treat', 'moderate'); - $forcemoderationt = $this->get_config('forcemoderationt', 60); + $forcemoderationt = intval($this->get_config('forcemoderationt', 60)); $forcemoderationt_treat = $this->get_config('forcemoderationt_treat', 'moderate'); - $links_moderate = $this->get_config('links_moderate', 10); - $links_reject = $this->get_config('links_reject', 20); + $links_moderate = intval($this->get_config('links_moderate', 10)); + $links_reject = intval($this->get_config('links_reject', 20)); + $timeout = $this->get_config('comment_timeout',false); + $timeout_type = $this->get_config('timeout_type','fix'); + $timeout_value = intval($this->get_config('timeout_value',30)); if (function_exists('imagettftext') && function_exists('imagejpeg')) { $max_char = 5; @@ -859,6 +889,19 @@ class serendipity_event_spamblock extends serendipity_event switch($event) { + case 'entry_display': + if (!is_array($eventData)) { + return false; + } + // get a word count and save it in SESSION, because entry_display is called after saveComment + if ($timeout && $addData['extended']) { + $_SESSION['serendipity_entry_wordcount'] = str_word_count($eventData[0]['body']) + str_word_count($eventData[0]['extended']); + } else { + if (isset($_SESSION['serendipity_entry_wordcount'])) unset( $_SESSION['serendipity_entry_wordcount']); + } + + break; + case 'fetchcomments': if (is_array($eventData) && !$_SESSION['serendipityAuthedUser'] && serendipity_db_bool($this->get_config('hide_email', 'false'))) { // Will force emails to be not displayed in comments and RSS feed for comments. Will not apply to logged in admins (so not in the backend as well) @@ -891,6 +934,7 @@ class serendipity_event_spamblock extends serendipity_event $this->log($logfile, $eventData['id'], 'REJECTED', PLUGIN_EVENT_SPAMBLOCK_CSRF_REASON, $addData); $eventData = array('allow_comments' => false); $serendipity['messagestack']['comments'][] = PLUGIN_EVENT_SPAMBLOCK_CSRF_REASON; + return false; } } @@ -931,7 +975,7 @@ class serendipity_event_spamblock extends serendipity_event if (!is_array($auth)) { // Filter authors names, Filter URL, Filter Content, Filter Emails, Check for maximum number of links before rejecting // moderate false - if(false === $this->wordfilter($logfile, $eventData, $wordmatch, $addData, true)) { + if(false === $this->wordfilter($logfile, $eventData, $wordmatch, $addData)) { // already there #$this->log($logfile, $eventData['id'], 'REJECTED', PLUGIN_EVENT_SPAMBLOCK_FILTER_WORDS, $addData); // already there #$eventData = array('allow_comments' => false); // already there #$serendipity['messagestack']['emails'][] = PLUGIN_EVENT_SPAMBLOCK_ERROR_BODY; @@ -947,7 +991,6 @@ class serendipity_event_spamblock extends serendipity_event $eventData['status'] = 'confirm1'; $serendipity['csuccess'] = 'moderate'; $serendipity['moderate_reason'] = PLUGIN_EVENT_SPAMBLOCK_CHECKMAIL_VERIFICATION_MAIL; - return false; } } else { // User is allowed to post message, bypassing other checks as if he were logged in. @@ -990,7 +1033,9 @@ class serendipity_event_spamblock extends serendipity_event // Check if sender ip is matching trackback/pingback ip (ip validation) $trackback_ipvalidation_option = $this->get_config('trackback_ipvalidation','moderate'); if (($addData['type'] == 'TRACKBACK' || $addData['type'] == 'PINGBACK') && $trackback_ipvalidation_option != 'no') { - $this->IsHardcoreSpammer(); + // $this->IsHardcoreSpammer(); this will block every pingback address ?! + + // check for urls excluded from the ip check $exclude_urls = explode(';',$this->get_config('trackback_ipvalidation_url_exclude', $this->get_default_exclude_urls())); $found_exclude_url = false; foreach ($exclude_urls as $exclude_url) { @@ -1042,13 +1087,13 @@ class serendipity_event_spamblock extends serendipity_event if (!empty($akismet_apikey) && ($akismet == 'moderate' || $akismet == 'reject') && !isset($addData['skip_akismet'])) { $spam = $this->getBlacklist('akismet.com', $akismet_apikey, $eventData, $addData); if ($spam['is_spam'] !== false) { - $this->IsHardcoreSpammer(); if ($akismet == 'moderate') { $this->log($logfile, $eventData['id'], 'MODERATE', PLUGIN_EVENT_SPAMBLOCK_REASON_AKISMET_SPAMLIST . ': ' . $spam['message'], $addData); $eventData['moderate_comments'] = true; $serendipity['csuccess'] = 'moderate'; $serendipity['moderate_reason'] = PLUGIN_EVENT_SPAMBLOCK_ERROR_BODY . ' (Akismet)'; } else { + $this->IsHardcoreSpammer(); $this->log($logfile, $eventData['id'], 'REJECTED', PLUGIN_EVENT_SPAMBLOCK_REASON_AKISMET_SPAMLIST . ': ' . $spam['message'], $addData); $eventData = array('allow_comments' => false); $serendipity['messagestack']['comments'][] = PLUGIN_EVENT_SPAMBLOCK_ERROR_BODY; @@ -1100,15 +1145,6 @@ class serendipity_event_spamblock extends serendipity_event return false; } - // Check for maximum number of links before rejecting - $link_count = substr_count(strtolower($addData['comment']), 'http://'); - if ($links_reject > 0 && $link_count > $links_reject) { - $this->log($logfile, $eventData['id'], 'REJECTED', PLUGIN_EVENT_SPAMBLOCK_REASON_LINKS_REJECT, $addData); - $eventData = array('allow_comments' => false); - $serendipity['messagestack']['comments'][] = PLUGIN_EVENT_SPAMBLOCK_ERROR_BODY; - return false; - } - // Captcha checking if ($show_captcha && $addData['type'] == 'NORMAL') { if (!isset($_SESSION['spamblock']['captcha']) || !isset($serendipity['POST']['captcha']) || strtolower($serendipity['POST']['captcha']) != strtolower($_SESSION['spamblock']['captcha'])) { @@ -1117,17 +1153,15 @@ class serendipity_event_spamblock extends serendipity_event $serendipity['messagestack']['comments'][] = PLUGIN_EVENT_SPAMBLOCK_ERROR_CAPTCHAS; return false; } else { -// DEBUG -// $this->log($logfile, $eventData['id'], 'REJECTED', 'Captcha passed: ' . $serendipity['POST']['captcha'] . ' / ' . $_SESSION['spamblock']['captcha'] . ' // Source: ' . $_SERVER['REQUEST_URI'], $addData); + // $this->log($logfile, $eventData['id'], 'REJECTED', 'Captcha passed: ' . $serendipity['POST']['captcha'] . ' / ' . $_SESSION['spamblock']['captcha'] . ' // Source: ' . $_SERVER['REQUEST_URI'], $addData); } } else { -// DEBUG -// $this->log($logfile, $eventData['id'], 'REJECTED', 'Captcha not needed: ' . $serendipity['POST']['captcha'] . ' / ' . $_SESSION['spamblock']['captcha'] . ' // Source: ' . $_SERVER['REQUEST_URI'], $addData); + // $this->log($logfile, $eventData['id'], 'REJECTED', 'Captcha not needed: ' . $serendipity['POST']['captcha'] . ' / ' . $_SESSION['spamblock']['captcha'] . ' // Source: ' . $_SERVER['REQUEST_URI'], $addData); } // Check for forced comment moderation (X days) - if ($addData['type'] == 'NORMAL' && $moderation_auto == true && ( - ( $forcemoderation == 0 ) || + if ($addData['type'] == 'NORMAL' && $moderation_auto == true && ( + ( $forcemoderation == 0 ) || ( $forcemoderation > 0 && $eventData['timestamp'] < (time() - ($forcemoderation * 60 * 60 * 24)) ) ) ) { $this->log($logfile, $eventData['id'], $forcemoderation_treat, PLUGIN_EVENT_SPAMBLOCK_REASON_FORCEMODERATION, $addData); if ($forcemoderation_treat == 'reject') { @@ -1155,14 +1189,6 @@ class serendipity_event_spamblock extends serendipity_event } } - // Check for maximum number of links before forcing moderation - if ($links_moderate > 0 && $link_count > $links_moderate) { - $this->log($logfile, $eventData['id'], 'REJECTED', PLUGIN_EVENT_SPAMBLOCK_REASON_LINKS_MODERATE, $addData); - $eventData['moderate_comments'] = true; - $serendipity['csuccess'] = 'moderate'; - $serendipity['moderate_reason'] = PLUGIN_EVENT_SPAMBLOCK_REASON_LINKS_MODERATE; - } - // Check for identical comments. We allow to bypass trackbacks from our server to our own blog. if ( $this->get_config('bodyclone', false) === true && $_SERVER['REMOTE_ADDR'] != $_SERVER['SERVER_ADDR'] && $addData['type'] == 'NORMAL') { $query = "SELECT count(id) AS counter FROM {$serendipity['dbPrefix']}comments WHERE type = '" . $addData['type'] . "' AND body = '" . serendipity_db_escape_string($addData['comment']) . "'"; @@ -1200,6 +1226,7 @@ class serendipity_event_spamblock extends serendipity_event // Check invalid email if ($addData['type'] == 'NORMAL' && serendipity_db_bool($this->get_config('checkmail', 'false'))) { if (!empty($addData['email']) && strstr($addData['email'], '@') === false) { + // todo: only an '@' is no proper check of an email ... $this->log($logfile, $eventData['id'], 'REJECTED', PLUGIN_EVENT_SPAMBLOCK_REASON_CHECKMAIL, $addData); $eventData = array('allow_comments' => false); $serendipity['messagestack']['comments'][] = PLUGIN_EVENT_SPAMBLOCK_REASON_CHECKMAIL; @@ -1207,6 +1234,39 @@ class serendipity_event_spamblock extends serendipity_event } } + // Check for comment timeout + if ($timeout) { + + // time passed in seconds since displaying the article + $usedtime = time() - $_SESSION['serendipity_comment_timeout']; + + switch ($timeout_type) { + case 'fix': + if ($usedtime < $timeout_value) { + $this->log($logfile, $eventData['id'], 'REJECTED', PLUGIN_EVENT_SPAMBLOCK_REASON_TIMEOUT, $addData); + $eventData = array('allow_comments' => false); + $serendipity['messagestack']['comments'][] = PLUGIN_EVENT_SPAMBLOCK_REASON_TIMEOUT; + return false; + } + break; + + case 'adaptive': + // reading time for (value) words per minute + $readtime = $_SESSION['serendipity_entry_wordcount'] * 60 / $timeout_value; + + // writing time for (value) chars per minute + $writetime = strlen($addData['comment']) * 60 / $timeout_value; + + if ($usedtime < $readtime + $writetime) { + $this->log($logfile, $eventData['id'], 'REJECTED', PLUGIN_EVENT_SPAMBLOCK_REASON_TIMEOUT, $addData); + $eventData = array('allow_comments' => false); + $serendipity['messagestack']['comments'][] = PLUGIN_EVENT_SPAMBLOCK_REASON_TIMEOUT; + return false; + } + break; + } + } + if ($eventData['moderate_comments'] == true) { return false; } @@ -1226,6 +1286,9 @@ class serendipity_event_spamblock extends serendipity_event echo serendipity_setFormToken('form'); } + if ($timeout) $_SESSION['serendipity_comment_timeout'] = time(); + + // Check whether to allow comments from registered authors if (serendipity_userLoggedIn() && $this->inGroup()) { return true; @@ -1404,7 +1467,7 @@ class serendipity_event_spamblock extends serendipity_event /** * wordfilter, email and additional link check moved to this function, to allow comment user to opt-in (verify_once), but reject all truly spam comments before. **/ - function wordfilter($logfile, &$eventData, $wordmatch, $addData, $ftc = false) + function wordfilter($logfile, &$eventData, $wordmatch, $addData) { global $serendipity; @@ -1511,17 +1574,23 @@ class serendipity_event_spamblock extends serendipity_event } } // Content filtering end - if($ftc) { - // Check for maximum number of links before rejecting - $link_count = substr_count(strtolower($addData['comment']), 'http://'); - $links_reject = $this->get_config('links_reject', 20); - if ($links_reject > 0 && $link_count > $links_reject) { - $this->log($logfile, $eventData['id'], 'REJECTED', PLUGIN_EVENT_SPAMBLOCK_REASON_LINKS_REJECT, $addData); - $eventData = array('allow_comments' => false); - $serendipity['messagestack']['comments'][] = PLUGIN_EVENT_SPAMBLOCK_ERROR_BODY; - return false; - } + // Check for maximum number of links in comment body to reject + $link_count = substr_count(strtolower($addData['comment']), 'http://'); + if ($links_reject > 0 && $link_count > $links_reject) { + $this->log($logfile, $eventData['id'], 'REJECTED', PLUGIN_EVENT_SPAMBLOCK_REASON_LINKS_REJECT, $addData); + $eventData = array('allow_comments' => false); + $serendipity['messagestack']['comments'][] = PLUGIN_EVENT_SPAMBLOCK_ERROR_BODY; + return false; } + + // Check for maximum number of links before forcing moderation + if ($links_moderate > 0 && $link_count > $links_moderate) { + $this->log($logfile, $eventData['id'], 'REJECTED', PLUGIN_EVENT_SPAMBLOCK_REASON_LINKS_MODERATE, $addData); + $eventData['moderate_comments'] = true; + $serendipity['csuccess'] = 'moderate'; + $serendipity['moderate_reason'] = PLUGIN_EVENT_SPAMBLOCK_REASON_LINKS_MODERATE; + } + } // function wordfilter end