diff options
| -rw-r--r-- | inc/anti-bot.php | 236 | ||||
| -rw-r--r-- | inc/functions.php | 161 | ||||
| -rw-r--r-- | inc/image.php | 1 | ||||
| -rw-r--r-- | inc/lib/Twig/Extensions/Extension/Tinyboard.php | 3 | ||||
| -rw-r--r-- | templates/post_form.html | 51 |
5 files changed, 274 insertions, 178 deletions
diff --git a/inc/anti-bot.php b/inc/anti-bot.php new file mode 100644 index 00000000..e8c26113 --- /dev/null +++ b/inc/anti-bot.php @@ -0,0 +1,236 @@ +<?php + +/* + * Copyright (c) 2010-2012 Tinyboard Development Group + */ + +if(realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { + // You cannot request this file directly. + exit; +} + +$hidden_inputs_twig = array(); + +class AntiBot { + public $inputs = array(), $index = 0; + private $salt; + + public static function randomString($length, $uppercase = false, $special_chars = false) { + $chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + if($uppercase) + $chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + if($special_chars) + $chars .= ' ~!@#$%^&*()_+,./;\'[]\\{}|:"<>?=-` '; + + $chars = str_split($chars); + + $ch = array(); + + // fill up $ch until we reach $length + while(count($ch) < $length) { + $n = $length - count($ch); + $keys = array_rand($chars, $n > count($chars) ? count($chars) : $n); + if($n == 1) { + $ch[] = $chars[$keys]; + break; + } + shuffle($keys); + foreach($keys as $key) + $ch[] = $chars[$key]; + } + + $chars = $ch; + + return implode('', $chars); + } + + public static function make_confusing($string) { + $chars = str_split($string); + + foreach($chars as &$c) { + if(rand(0, 2) != 0) + continue; + $c = mb_encode_numericentity($c, array(0, 0xffff, 0, 0xffff), 'UTF-8'); + } + + return implode('', $chars); + } + + public function __construct(array $salt = array()) { + global $config; + + if(!empty($salt)) { + // create a salted hash of the "extra salt" + $this->salt = implode(':', $salt); + } else { + $this->salt = ''; + } + + shuffle($config['spam']['hidden_input_names']); + + $input_count = rand($config['spam']['hidden_inputs_min'], $config['spam']['hidden_inputs_max']); + $hidden_input_names_x = 0; + + for($x = 0; $x < $input_count ; $x++) { + if($hidden_input_names_x === false || rand(0, 2) == 0) { + // Use an obscure name + $name = $this->randomString(rand(10, 40)); + } else { + // Use a pre-defined confusing name + $name = $config['spam']['hidden_input_names'][$hidden_input_names_x++]; + if($hidden_input_names_x >= count($config['spam']['hidden_input_names'])) + $hidden_input_names_x = false; + } + + if(rand(0, 2) == 0) { + // Value must be null + $this->inputs[$name] = ''; + } elseif(rand(0, 4) == 0) { + // Numeric value + $this->inputs[$name] = (string)rand(0, 100); + } else { + // Obscure value + $this->inputs[$name] = $this->randomString(rand(5, 100)); + } + } + } + + public function html($count = false) { + $elements = array( + '<input type="hidden" name="%name%" value="%value%">', + '<input type="hidden" value="%value%" name="%name%">', + '<input style="display:none" type="text" name="%name%" value="%value%">', + '<input style="display:none" type="text" value="%value%" name="%name%">', + '<span style="display:none"><input type="text" name="%name%" value="%value%"></span>', + '<div style="display:none"><input type="text" name="%name%" value="%value%"></div>', + '<div style="display:none"><input type="text" name="%name%" value="%value%"></div>', + '<textarea style="display:none" name="%name%">%value%</textarea>', + '<textarea name="%name%" style="display:none">%value%</textarea>' + ); + + $html = ''; + + if($count == 0) { + // all elements + $inputs = array_slice($this->inputs, $this->index); + } else { + $inputs = array_slice($this->inputs, $this->index, $count); + } + $this->index += count($inputs); + + foreach($inputs as $name => $value) { + $element = false; + while(!$element) { + $element = $elements[array_rand($elements)]; + if(strpos($element, 'textarea') !== false && $value == '') { + // There have been some issues with mobile web browsers and empty <textarea>'s. + $element = false; + } + } + + $element = str_replace('%name%', utf8tohtml($name), $element); + + if(rand(0, 2) == 0) + $value = $this->make_confusing($value); + else + $value = utf8tohtml($value); + + $element = str_replace('%value%', $value, $element); + + $html .= $element; + } + + return $html; + } + + public function hash() { + global $config; + + // This is the tricky part: create a hash to validate it after + // First, sort the keys in alphabetical order (A-Z) + $inputs = $this->inputs; + ksort($inputs); + + $hash = ''; + // Iterate through each input + foreach($inputs as $name => $value) { + $hash .= $name . '=' . $value; + } + // Add a salt to the hash + $hash .= $config['cookies']['salt']; + + // Use SHA1 for the hash + return sha1($hash . $this->salt); + } +};; + + +function hiddenInputs(array $salt, $print_the_rest = false) { + global $hidden_inputs_twig; + + $salt_str = implode(':', $salt); + + if(!isset($hidden_inputs_twig[$salt_str])) + $hidden_inputs_twig[$salt_str] = new AntiBot($salt); + + if($print_the_rest) + return $hidden_inputs_twig[$salt_str]->html(0); + else + return $hidden_inputs_twig[$salt_str]->html(rand(1, 5)); +} + +function hiddenInputsHash(array $salt) { + global $hidden_inputs_twig; + + $salt_str = implode(':', $salt); + + if(!isset($hidden_inputs_twig[$salt_str])) + $hidden_inputs_twig[$salt_str] = new AntiBot($salt); + + return $hidden_inputs_twig[$salt_str]->hash(); +} + +function checkSpam(array $extra_salt = array()) { + global $config; + + if(!isset($_POST['hash'])) + return true; + + $hash = $_POST['hash']; + + if(!empty($extra_salt)) { + // create a salted hash of the "extra salt" + $extra_salt = implode(':', $extra_salt); + } else { + $extra_salt = ''; + } + + // Reconsturct the $inputs array + $inputs = array(); + + foreach($_POST as $name => $value) { + if(in_array($name, $config['spam']['valid_inputs'])) + continue; + + $inputs[$name] = $value; + } + + // Sort the inputs in alphabetical order (A-Z) + ksort($inputs); + + $_hash = ''; + + // Iterate through each input + foreach($inputs as $name => $value) { + $_hash .= $name . '=' . $value; + } + + // Add a salt to the hash + $_hash .= $config['cookies']['salt']; + + // Use SHA1 for the hash + $_hash = sha1($_hash . $extra_salt); + + return $hash != $_hash; +} + diff --git a/inc/functions.php b/inc/functions.php index d7fcc3dd..3761b608 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -13,6 +13,7 @@ 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 @@ -1160,166 +1161,6 @@ function checkMute() { } } -function createHiddenInputs($extra_salt = array()) { - global $config; - - if(!empty($extra_salt)) { - // create a salted hash of the "extra salt" - $extra_salt = implode(':', $extra_salt); - } else { - $extra_salt = ''; - } - - $inputs = array(); - - shuffle($config['spam']['hidden_input_names']); - $hidden_input_names_x = 0; - - $input_count = rand($config['spam']['hidden_inputs_min'], $config['spam']['hidden_inputs_max']); - for($x=0;$x<$input_count;$x++) { - if(rand(0, 2) == 0 || $hidden_input_names_x < 0) { - // Use an obscure name - $name = strtolower(substr(base64_encode(sha1(rand(), true)), 0, rand(2, 20))); - - } else { - // Use a pre-defined confusing name - $name = $config['spam']['hidden_input_names'][$hidden_input_names_x++]; - if($hidden_input_names_x >= count($config['spam']['hidden_input_names'])) - $hidden_input_names_x = -1; - } - - if(rand(0, 2) == 0) { - // Value must be null - $inputs[$name] = ''; - } elseif(rand(0, 4) == 0) { - // Numeric value - $inputs[$name] = rand(0, 100); - } else { - // Obscure value - $inputs[$name] = substr(base64_encode(sha1(rand(), true) . sha1(rand(), true)), 0, rand(2, 54)); - } - } - - $content = ''; - foreach($inputs as $name => $value) { - $display_type = rand(0, 8); - - switch($display_type) { - case 0: - $content .= '<input type="hidden" name="' . htmlspecialchars($name) . '" value="' . - htmlspecialchars($value) . '" />'; - break; - case 1: - $content .= '<input style="display:none" name="' . htmlspecialchars($name) . '" value="' . - htmlspecialchars($value) . '" />'; - break; - case 2: - $content .= '<input type="hidden" value="' . htmlspecialchars($value) . '" name="' . - htmlspecialchars($name) . '" />'; - break; - case 3: - $content .= '<input type="hidden" name="' . htmlspecialchars($name) . '" value="' . - htmlspecialchars($value) . '" />'; - break; - case 4: - $content .= '<span style="display:none"><input type="text" name="' . htmlspecialchars($name) . '" value="' . - htmlspecialchars($value) .'" /></span>'; - break; - case 5: - $content .= '<div style="display:none"><input type="text" name="' . htmlspecialchars($name) . '" value="' . - htmlspecialchars($value) . '" /></div>'; - break; - case 6: - if(!empty($value)) - $content .= '<textarea style="display:none" name="' . htmlspecialchars($name) . '">' . - htmlspecialchars($value) . '</textarea>'; - else - $content .= '<input type="hidden" name="' . htmlspecialchars($name) . '" value="' . - htmlspecialchars($value) . '" />'; - break; - case 7: - if(!empty($value)) - $content .= '<textarea name="' . htmlspecialchars($name) . '" style="display:none">' . - htmlspecialchars($value) . '</textarea>'; - else - $content .= '<input type="hidden" name="' . htmlspecialchars($name) . '" value="' . - htmlspecialchars($value) . '" />'; - break; - case 8: - $content .= '<div style="display:none"><textarea name="' . htmlspecialchars($name) . '" style="display:none">' . - htmlspecialchars($value) . '</textarea></div>'; - break; - } - } - - // Create a hash to validate it after - // This is the tricky part. - - // First, sort the keys in alphabetical order (A-Z) - ksort($inputs); - - $hash = ''; - - // Iterate through each input - foreach($inputs as $name => $value) { - $hash .= $name . '=' . $value; - } - - // Add a salt to the hash - $hash .= $config['cookies']['salt']; - - // Use SHA1 for the hash - $hash = sha1($hash . $extra_salt); - - // Append it to the HTML - $content .= '<input type="hidden" name="hash" value="' . $hash . '" />'; - - return $content; -} - -function checkSpam($extra_salt = array()) { - global $config; - - if(!isset($_POST['hash'])) - return true; - - $hash = $_POST['hash']; - - if(!empty($extra_salt)) { - // create a salted hash of the "extra salt" - $extra_salt = implode(':', $extra_salt); - } else { - $extra_salt = ''; - } - - // Reconsturct the $inputs array - $inputs = array(); - - foreach($_POST as $name => $value) { - if(in_array($name, $config['spam']['valid_inputs'])) - continue; - - $inputs[$name] = $value; - } - - // Sort the inputs in alphabetical order (A-Z) - ksort($inputs); - - $_hash = ''; - - // Iterate through each input - foreach($inputs as $name => $value) { - $_hash .= $name . '=' . $value; - } - - // Add a salt to the hash - $_hash .= $config['cookies']['salt']; - - // Use SHA1 for the hash - $_hash = sha1($_hash . $extra_salt); - - return $hash != $_hash; -} function buildIndex() { global $board, $config; diff --git a/inc/image.php b/inc/image.php index f6bc3e7c..d1c292dc 100644 --- a/inc/image.php +++ b/inc/image.php @@ -194,7 +194,6 @@ class ImageImagick extends ImageBase { foreach($this->original as $frame) { $delay += $frame->getImageDelay(); - //if($i < $config['thumb_keep_animation_frames']) { if(in_array($i, $keep_frames)) { // $frame->scaleImage($this->width, $this->height, false); $frame->sampleImage($this->width, $this->height); diff --git a/inc/lib/Twig/Extensions/Extension/Tinyboard.php b/inc/lib/Twig/Extensions/Extension/Tinyboard.php index 0cf7705a..7e2149e5 100644 --- a/inc/lib/Twig/Extensions/Extension/Tinyboard.php +++ b/inc/lib/Twig/Extensions/Extension/Tinyboard.php @@ -35,7 +35,8 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension return Array( 'time' => new Twig_Filter_Function('time', array('needs_environment' => false)), 'timezone' => new Twig_Filter_Function('twig_timezone_function', array('needs_environment' => false)), - 'createHiddenInputs' => new Twig_Filter_Function('createHiddenInputs', array('needs_environment' => false)) + 'hiddenInputs' => new Twig_Filter_Function('hiddenInputs', array('needs_environment' => false)), + 'hiddenInputsHash' => new Twig_Filter_Function('hiddenInputsHash', array('needs_environment' => false)) ); } diff --git a/templates/post_form.html b/templates/post_form.html index 1ac1dfcf..69b179fd 100644 --- a/templates/post_form.html +++ b/templates/post_form.html @@ -1,49 +1,60 @@ <form name="post" onsubmit="return dopost(this);" enctype="multipart/form-data" action="{{ config.post_url }}" method="post"> -{{ createHiddenInputs([board.uri, id]) }} -{% if id %}<input type="hidden" name="thread" value="{{ id }}" />{% endif %} -<input type="hidden" name="board" value="{{ board.uri }}" /> -{% if mod %}<input type="hidden" name="mod" value="1" />{% endif %} +{{ hiddenInputs([board.uri, id]) }} +{% if id %}<input type="hidden" name="thread" value="{{ id }}">{% endif %} +{{ hiddenInputs([board.uri, id]) }} +<input type="hidden" name="board" value="{{ board.uri }}"> +{{ hiddenInputs([board.uri, id]) }} +{% 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]) }} </th> <td> - <input type="text" name="name" size="25" maxlength="50" autocomplete="off" /> + <input type="text" name="name" size="25" maxlength="50" autocomplete="off"> + {{ hiddenInputs([board.uri, id]) }} </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]) }} </th> <td> - <input type="text" name="email" size="25" maxlength="40" autocomplete="off" /> + <input type="text" name="email" size="25" maxlength="40" autocomplete="off"> + {{ hiddenInputs([board.uri, id]) }} </td> </tr>{% endif %} <tr> <th> {% trans %}Subject{% endtrans %} + {{ hiddenInputs([board.uri, id]) }} </th> <td> - <input style="float:left;" type="text" name="subject" size="25" maxlength="100" autocomplete="off" /> - <input accesskey="s" style="margin-left:2px;" type="submit" name="post" value="{% if id %}{{ config.button_reply }}{% else %}{{ config.button_newtopic }}{% endif %}" />{% if config.spoiler_images %} <input id="spoiler" name="spoiler" type="checkbox" /> <label for="spoiler">{% trans %}Spoiler Image{% endtrans %}</label>{% endif %} + <input style="float:left;" type="text" name="subject" size="25" maxlength="100" autocomplete="off"> + <input accesskey="s" style="margin-left:2px;" type="submit" name="post" value="{% if id %}{{ config.button_reply }}{% else %}{{ config.button_newtopic }}{% endif %}" />{% if config.spoiler_images %} <input id="spoiler" name="spoiler" type="checkbox"> <label for="spoiler">{% trans %}Spoiler Image{% endtrans %}</label>{% endif %} </td> </tr> <tr> <th> {% trans %}Comment{% endtrans %} + {{ hiddenInputs([board.uri, id]) }} </th> <td> <textarea name="body" id="body" rows="5" cols="35"></textarea> + {{ hiddenInputs([board.uri, id]) }} </td> </tr> {% if config.recaptcha %} <tr> <th> {% trans %}Verification{% endtrans %} + {{ hiddenInputs([board.uri, id]) }} </th> <td> <script type="text/javascript" src="http://www.google.com/recaptcha/api/challenge?k={{ config.recaptcha_public }}"></script> + {{ hiddenInputs([board.uri, id]) }} </td> </tr> {% endif %} @@ -52,16 +63,19 @@ {% trans %}File{% endtrans %} </th> <td> - <input type="file" name="file" /> + <input type="file" name="file"> + {{ hiddenInputs([board.uri, id]) }} </td> </tr> {% if config.enable_embedding %} <tr> <th> {% trans %}Embed{% endtrans %} + {{ hiddenInputs([board.uri, id]) }} </th> <td> - <input type="text" name="embed" size="30" maxlength="120" autocomplete="off" /> + <input type="text" name="embed" size="30" maxlength="120" autocomplete="off"> + {{ hiddenInputs([board.uri, id]) }} </td> </tr> {% endif %} @@ -73,15 +87,15 @@ <td> {% if not id and post.mod|hasPermission(config.mod.sticky, board.uri) %}<div> <label for="sticky">{% trans %}Sticky{% endtrans %}</label> - <input title="{% trans %}Sticky{% endtrans %}" type="checkbox" name="sticky" id="sticky" /><br /> + <input title="{% trans %}Sticky{% endtrans %}" type="checkbox" name="sticky" id="sticky"><br> </div>{% endif %} {% if not id and post.mod|hasPermission(config.mod.lock, board.uri) %}<div> - <label for="lock">{% trans %}Lock{% endtrans %}</label><br /> - <input title="{% trans %}Lock{% endtrans %}" type="checkbox" name="lock" id="lock" /> + <label for="lock">{% trans %}Lock{% endtrans %}</label><br> + <input title="{% trans %}Lock{% endtrans %}" type="checkbox" name="lock" id="lock"> </div>{% endif %} {% if post.mod|hasPermission(config.mod.rawhtml, board.uri) %}<div> - <label for="raw">{% trans %}Raw HTML{% endtrans %}</label><br /> - <input title="{% trans %}Raw HTML{% endtrans %}" type="checkbox" name="raw" id="raw" /> + <label for="raw">{% trans %}Raw HTML{% endtrans %}</label><br> + <input title="{% trans %}Raw HTML{% endtrans %}" type="checkbox" name="raw" id="raw"> </div>{% endif %} </td> </tr> @@ -89,13 +103,18 @@ {% 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]) }} </th> <td> - <input type="password" name="password" size="12" maxlength="18" autocomplete="off" /> + <input type="password" name="password" size="12" maxlength="18" autocomplete="off"> <span class="unimportant">{% trans %}(For file deletion.){% endtrans %}</span> + {{ hiddenInputs([board.uri, id]) }} </td> </tr>{% endif %} </table> +{{ hiddenInputs([board.uri, id]) }} +<input type="hidden" name="hash" value="{{ hiddenInputsHash([board.uri, id]) }}"> +{{ hiddenInputs([board.uri, id], true) }} </form> <script type="text/javascript">{% raw %} |
