【问题标题】:How to properly add cross-site request forgery (CSRF) token using PHP如何使用 PHP 正确添加跨站请求伪造 (CSRF) 令牌
【发布时间】:2011-09-11 09:18:33
【问题描述】:

我正在尝试为我网站上的表单添加一些安全性。其中一种形式使用 AJAX,另一种是直接的“联系我们”形式。我正在尝试添加一个 CSRF 令牌。我遇到的问题是令牌有时只出现在 HTML“值”中。其余时间,该值为空。这是我在 AJAX 表单上使用的代码:

PHP:

if (!isset($_SESSION)) {
    session_start();
$_SESSION['formStarted'] = true;
}
if (!isset($_SESSION['token']))
{$token = md5(uniqid(rand(), TRUE));
$_SESSION['token'] = $token;

}

HTML

 <form>
//...
<input type="hidden" name="token" value="<?php echo $token; ?>" />
//...
</form>

有什么建议吗?

【问题讨论】:

  • 只是好奇,token_time 是干什么用的?
  • @zerkms 我目前没有使用token_time。我打算限制令牌有效的时间,但还没有完全实现代码。为了清楚起见,我已将其从上述问题中删除。
  • @Ken:所以用户可以在打开表单时获取案例,发布并获取无效令牌? (因为它已经失效了)
  • @zerkms:谢谢,但我有点困惑。有没有机会给我举个例子?
  • @Ken:当然。假设令牌在上午 10:00 到期。现在是上午 09:59。用户打开一个表单并获得一个令牌(仍然有效)。然后用户填写表格 2 分钟,然后发送。只要现在是上午 10:01 - 令牌被视为无效,因此用户会收到表单错误。

标签: php security session csrf


【解决方案1】:

对于安全代码,请不要以这种方式生成您的令牌:$token = md5(uniqid(rand(), TRUE));

试试这个:

生成 CSRF 令牌

PHP 7

session_start();
if (empty($_SESSION['token'])) {
    $_SESSION['token'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['token'];

旁注:my employer's open source projects 之一是将random_bytes()random_int() 反向移植到PHP 5 项目的倡议。它已获得 MIT 许可,可在 Github 和 Composer 上以 paragonie/random_compat 的形式获得。

PHP 5.3+(或使用 ext-mcrypt)

session_start();
if (empty($_SESSION['token'])) {
    if (function_exists('mcrypt_create_iv')) {
        $_SESSION['token'] = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
    } else {
        $_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(32));
    }
}
$token = $_SESSION['token'];

验证 CSRF 令牌

不要只使用== 甚至===,而是使用hash_equals()(仅限PHP 5.6+,但可用于带有hash-compat 库的早期版本)。

if (!empty($_POST['token'])) {
    if (hash_equals($_SESSION['token'], $_POST['token'])) {
         // Proceed to process the form data
    } else {
         // Log this as a warning and keep an eye on these attempts
    }
}

使用 Per-Form 令牌更进一步

您可以使用hash_hmac() 进一步限制令牌仅可用于特定表单。 HMAC 是一种特殊的键控散列函数,即使使用较弱的散列函数(例如 MD5),也可以安全使用。不过,我建议改用 SHA-2 系列哈希函数。

首先,生成第二个令牌用作 HMAC 密钥,然后使用如下逻辑来呈现它:

<input type="hidden" name="token" value="<?php
    echo hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
?>" />

然后在验证令牌时使用全等运算:

$calc = hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
if (hash_equals($calc, $_POST['token'])) {
    // Continue...
}

在不知道$_SESSION['second_token'] 的情况下,无法在另一个上下文中重复使用为一种表单生成的令牌。 重要的是您使用单独的令牌作为 HMAC 密钥,而不是您刚刚放在页面上的那个。

奖励:混合方法 + Twig 集成

任何使用Twig templating engine 的人都可以通过将此过滤器添加到他们的 Twig 环境中来从简化的双重策略中受益:

$twigEnv->addFunction(
    new \Twig_SimpleFunction(
        'form_token',
        function($lock_to = null) {
            if (empty($_SESSION['token'])) {
                $_SESSION['token'] = bin2hex(random_bytes(32));
            }
            if (empty($_SESSION['token2'])) {
                $_SESSION['token2'] = random_bytes(32);
            }
            if (empty($lock_to)) {
                return $_SESSION['token'];
            }
            return hash_hmac('sha256', $lock_to, $_SESSION['token2']);
        }
    )
);

使用这个 Twig 函数,您可以像这样使用这两种通用标记:

<input type="hidden" name="token" value="{{ form_token() }}" />

或锁定变体:

<input type="hidden" name="token" value="{{ form_token('/my_form.php') }}" />

Twig 只关心模板渲染;您仍然必须正确验证令牌。在我看来,Twig 策略提供了更大的灵活性和简单性,同时保持了最大安全性的可能性。


一次性 CSRF 令牌

如果您有一个安全要求,即允许每个 CSRF 令牌只能使用一次,那么最简单的策略是在每次成功验证后重新生成它。但是,这样做会使之前的每一个令牌都失效,这与同时浏览多个标签的人不能很好地混合。

Paragon Initiative Enterprises 为这些极端案例维护Anti-CSRF library。它仅适用于一次性使用的 per-form 令牌。当会话数据中存储了足够的令牌时(默认配置:65535),它将首先循环出最旧的未兑换令牌。

【讨论】:

  • 不错,但是如何在用户提交表单后更改 $token?在您的情况下,一个令牌用于用户会话。
  • 仔细看看github.com/paragonie/anti-csrf是如何实现的。令牌是一次性的,但它可以存储多个。
  • @ScottArciszewski 您如何看待从会话 ID 生成消息摘要和秘密,然后将收到的 CSRF 令牌摘要与再次哈希会话 ID 和我之前的秘密进行比较?我希望你明白我的意思。
  • 我有一个关于验证 CSRF 令牌的问题。如果 $_POST['token'] 为空,我们不应该继续,因为这个 post 请求是在没有令牌的情况下发送的,对吧?
  • 因为它将被回显到 HTML 表单中,并且您希望它是不可预测的,因此攻击者不能只是伪造它。您实际上在这里实现了质询-响应身份验证,而不仅仅是“是的,这种形式是合法的”,因为攻击者可以欺骗它。
【解决方案2】:

安全警告md5(uniqid(rand(), TRUE)) 不是生成随机数的安全方法。请参阅this answer 了解更多信息以及利用加密安全随机数生成器的解决方案。

看起来你的 if 需要一个 else。

if (!isset($_SESSION['token'])) {
    $token = md5(uniqid(rand(), TRUE));
    $_SESSION['token'] = $token;
    $_SESSION['token_time'] = time();
}
else
{
    $token = $_SESSION['token'];
}

【讨论】:

  • 注意:我不相信 md5(uniqid(rand(), TRUE)); 的安全上下文。
  • 直截了当地这是对这个家伙问题的正确答案。另一个获得 300 多票的答案解决了 OP 没有提出的问题。
【解决方案3】:

变量$token 在会话中时没有被检索到

【讨论】:

    猜你喜欢
    • 2021-10-17
    • 1970-01-01
    • 2013-07-17
    • 2019-05-26
    • 1970-01-01
    • 2011-05-17
    • 2012-08-14
    • 1970-01-01
    • 2016-02-19
    相关资源
    最近更新 更多