这个PHP验证码脚本有什么问题


What is wrong with this PHP captcha script?

我已经使用这个脚本很长时间了,它在 99% 下运行良好。这对用户来说既简单又清晰,我想继续使用它。

但是,偶尔一个稀疏的用户告诉我,当数字正确时,系统不接受他的验证码(错误的代码)。每次我都在检查他们的 cookie 设置、清除缓存等,但在这些情况下似乎没有任何效果。

因此,我的问题是,此脚本的代码中是否有任何原因可以解释特殊情况下的故障?

session_start();
$randomnr = rand(1000, 9999);
$_SESSION['randomnr2'] = md5($randomnr);
$im = imagecreatetruecolor(100, 28);
$white = imagecolorallocate($im, 255, 255, 255);
$grey = imagecolorallocate($im, 128, 128, 128);
$black = imagecolorallocate($im, 0,0,0);
imagefilledrectangle($im, 0, 0, 200, 35, $black);
$font = '/img/captcha/font.ttf';
imagettftext($im, 30, 0, 10, 40, $grey, $font, $randomnr);
imagettftext($im, 20, 3, 18, 25, $white, $font, $randomnr);
// Prevent caching
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past3
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header ("Content-type: image/gif");
imagegif($im);
imagedestroy($im);

在我的表单中,我调用此脚本作为验证码图像的来源。发送表单后,验证码将按以下方式检查:

if(md5($_POST['norobot']) != $_SESSION['randomnr2']) {
    echo 'Wrong captcha!';
}

请注意,session_start();是在表单页面和表单结果页面上调用的。

如果有人能查明此脚本中的潜在错误原因,我将不胜感激!

PS:我知道验证码脚本的缺点。我知道某些机器人仍然可以读出它们。我不想使用 Recaptcha,因为这对我的用户来说太难了(不同的语言 + 很多时候的老用户)。我也知道 md5 很容易解密。


编辑 编辑

编辑


根据乌戈·梅达(Ugo Méda)的话,我一直在做一些实验。这是我创建的(为方便起见进行了简化):

表格

// Insert a random number of four digits into database, along with current time
$query   = 'INSERT INTO captcha (number, created_date, posted) VALUES ("'.rand(1000, 9999).'", NOW(),0)';
$result  = mysql_query($query);
// Retrieve the id of the inserted number
$captcha_uid = mysql_insert_id();
$output .= '<label for="norobot"> Enter spam protection code';
// Send id to captcha script
$output .= '<img src="/img/captcha/captcha.php?number='.$captcha_uid.'" />'; 
// Hidden field with id 
$output .= '<input type="hidden" name="captcha_uid" value="'.$captcha_uid.'" />'; 
$output .= '<input type="text" name="norobot" class="norobot" id="norobot" maxlength="4" required  />';
$output .= '</label>';
echo $output;

验证码脚本

$font = '/img/captcha/font.ttf';
connect();
// Find the number associated to the captcha id
$query = 'SELECT number FROM captcha WHERE uid = "'.mysql_real_escape_string($_GET['number']).'" LIMIT 1';
$result = mysql_query($query) or trigger_error(__FUNCTION__.'<hr />'.mysql_error().'<hr />'.$query);
if (mysql_num_rows($result) != 0){          
    while($row = mysql_fetch_assoc($result)){
        $number = $row['number'];
    }
} 
disconnect();
$im     = imagecreatetruecolor(100, 28);
$white  = imagecolorallocate($im, 255, 255, 255);
$grey   = imagecolorallocate($im, 128, 128, 128);
$black  = imagecolorallocate($im, 0,0,0);
imagefilledrectangle($im, 0, 0, 200, 35, $black);
imagettftext($im, 30, 0, 10, 40, $grey, $font, $number);
imagettftext($im, 20, 3, 18, 25, $white, $font, $number);
// Generate the image from the number retrieved out of database
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past3
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header ("Content-type: image/gif");
imagegif($im);
imagedestroy($im);

表单的结果

function get_captcha_number($captcha_uid) {
    $query = 'SELECT number FROM captcha WHERE uid = "'.mysql_real_escape_string($captcha_uid).'" LIMIT 1';
    $result = mysql_query($query);
    if (mysql_num_rows($result) != 0){          
        while($row = mysql_fetch_assoc($result)){
            return $row['number'];
        }
    } 
    // Here I would later also enter the DELETE QUERY mentioned above...
}
if($_POST['norobot'] != get_captcha_number($_POST['captcha_uid'])) {
    echo 'Captcha error'
    exit;
}

这效果很好,所以非常感谢这个解决方案。

但是,我在这里看到了一些潜在的缺点。我注意到至少有 4 个查询,并且对于我们正在做的事情来说感觉有些资源密集。此外,当用户多次重新加载同一页面(只是为了成为一个混蛋)时,数据库会很快填满。当然,这一切都会在下次提交表格时删除,但尽管如此,你能和我一起讨论这个可能的替代方案吗?

我知道通常不应该加密/解密。但是,由于验证码本质上是有缺陷的(因为机器人的图像读出),我们不能通过加密和解密发送到captcha.php脚本的参数来简化该过程吗?

如果我们这样做(按照Alix Axel的加密/解密指令)会怎样:

1)加密一个随机的四位数字符,如下所示:

$key = 'encryption-password-only-present-within-the-application';
$string = rand(1000,9999);
$encrypted = base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, md5($key), $string, MCRYPT_MODE_CBC, md5(md5($key))));

2)将带有参数的加密号码发送到图像脚本中,并将其存储在隐藏字段中

<img src="/img/captcha.php?number="'.$encrypted.'" />
<input type="hidden" name="encrypted_number" value="'.$encrypted.'" />

3)解密验证码脚本中的数字(通过$ _GET发送)并从中生成图像

$decrypted = rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, md5($key), base64_decode($encrypted), MCRYPT_MODE_CBC, md5(md5($key))), "'0"); 

4)再次解密表单提交上的数字以与用户输入进行比较 $decrypted = rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, md5($key), base64_decode($encrypted), MCRYPT_MODE_CBC, md5(md5($key))), "''0");
if($_POST['norobot'] != $decrypted) { 回显"验证码错误! 退出; }

同意,这有点"通过默默无闻的安全性",但它似乎提供了一些基本的安全性并且仍然相当简单。或者这种加密/解密操作本身会过于资源密集吗?

有人对此有何评论吗?

不要只依赖 SESSION 值,原因有两个:

  • 您的会话可能会过期,因此在某些情况下
  • 不起作用
  • 如果用户打开具有相同页面的另一个选项卡,您将遇到奇怪的行为

使用某种令牌:

  • 在输出表单时生成一个随机ID,将其与预期数字(和当前日期/时间)一起放入数据库中
  • 使用此 ID 生成映像
  • 在表单中添加带有 ID 的隐藏输入
  • 收到 POST 后,从数据库中获取预期值并进行比较
  • 删除此令牌和所有旧令牌(例如WHERE token == %token AND datetime < DATE_SUB(NOW(), INTERVAL 1 HOUR)

有时,某些访问者可能落后于代理,或者他们的计算机上有一个插件/软件可以对某些文件进行双重请求。我在开发我的一个项目时发现了这一点,并且有一些我完全忘记的 Chrome 插件。

由于这种情况发生在

您的访客中如此之少,因此情况可能就是这种情况。以下是我调试问题的步骤(请记住,这是一个开发环境,我可以直接在站点上修改代码):

当访问者报告问题时,为他们启用"调试",这意味着我会将他们的 IP 添加到验证码生成器配置中的调试数组中。这将执行以下操作:

  1. 以微时间格式获取图像的生成时间。
  2. 在文件系统的某个位置写入日志文件中,每个请求对验证码页面的请求,格式类似于:ip|microtime|random_numbers
  3. 检查日志中是否有用户 IP 地址发出的请求,并查看在彼此大约 10 秒的范围内是否有任何关闭的请求。如果有,那么有一些东西正在向您的验证码页面发出第二个请求,并且它正在生成访问者看不到的新代码。

此外,您还需要确保在清除用户的缓存后,用户在每次刷新页面时都会看到不同的数字。浏览器端可能会有一个古怪的行为,但它可能会显示旧的缓存副本(在 Firefox 上看到它,您必须清除缓存,重新启动浏览器,再次清除缓存,然后它工作正常)。

如果是这种情况,您可以对脚本执行简单的基于时间的添加,以执行以下操作:

生成新的验证码图像时,请检查会话中是否已设置验证码编号。如果设置了它们,请检查它们生成的时间,如果小于 10 秒,只需显示相同的数字。如果超过 10 秒,则显示新数字。 此方法的唯一警告是,每次使用验证码变量时,都必须在会话中取消设置该变量。

示例代码为:

<?php
// begin generating captcha:
session_start();
if (
   empty($_SESSION['randomnr2']) // there is no captcha set
   || empty($_SESSION['randomnr2_time'])  // there is no time set
   || ( time() - $_SESSION['randomnr2_time']  > 10 ) // time is more than 10 secs
) {
   $randomnr = rand(1000, 9999);
   $_SESSION['randomnr2'] = md5($randomnr);
   $_SESSION['randomnr2_time'] = microtime(true); // this is the time it was 
                                                  // generated. You can use it 
                                                  // to write in the log file
}

// ...
?>