【发布时间】:2011-02-11 07:34:48
【问题描述】:
最近几天我已经阅读了有关如何防止 CSRF 攻击的信息。我将在每次页面加载中更新令牌,将令牌保存在会话中并在提交表单时进行检查。
但是如果用户有,假设我的网站打开了 3 个选项卡,而我只是将最后一个令牌存储在会话中?这将用另一个令牌覆盖该令牌,并且某些后操作将失败。
我是否需要将所有令牌存储在会话中,还是有更好的解决方案来使其正常工作?
【问题讨论】:
最近几天我已经阅读了有关如何防止 CSRF 攻击的信息。我将在每次页面加载中更新令牌,将令牌保存在会话中并在提交表单时进行检查。
但是如果用户有,假设我的网站打开了 3 个选项卡,而我只是将最后一个令牌存储在会话中?这将用另一个令牌覆盖该令牌,并且某些后操作将失败。
我是否需要将所有令牌存储在会话中,还是有更好的解决方案来使其正常工作?
【问题讨论】:
是的,使用存储令牌方法,您必须保留所有生成的令牌,以防它们在任何时候返回。单个存储令牌不仅对多个浏览器选项卡/窗口失败,而且对后退/前进导航也失败。您通常希望通过使旧令牌过期(按年龄和/或此后发行的令牌数量)来管理潜在的存储爆炸。
另一种完全避免令牌存储的方法是发出使用服务器端密钥生成的签名令牌。然后,当您取回令牌时,您可以检查签名,如果匹配,您就知道您已签名。例如:
// Only the server knows this string. Make it up randomly and keep it in deployment-specific
// settings, in an include file safely outside the webroot
//
$secret= 'qw9pDr$wEyq%^ynrUi2cNi3';
...
// Issue a signed token
//
$token= dechex(mt_rand());
$hash= hash_hmac('sha1', $token, $secret);
$signed= $token.'-'.$hash;
<input type="hidden" name="formkey" value="<?php echo htmlspecialchars($signed); ?>">
...
// Check a token was signed by us, on the way back in
//
$isok= FALSE;
$parts= explode('-', $_POST['formkey']);
if (count($parts)===2) {
list($token, $hash)= $parts;
if ($hash===hash_hmac('sha1', $token, $secret))
$isok= TRUE;
}
有了这个,如果你得到一个带有匹配签名的令牌,你就知道你生成了它。这本身并没有多大帮助,但是您可以在令牌中添加除随机性之外的额外内容,例如用户 ID:
$token= dechex($user->id).'.'.dechex(mt_rand())
...
if ($hash===hash_hmac('sha1', $token, $secret)) {
$userid= hexdec(explode('.', $token)[0]);
if ($userid===$user->id)
$isok= TRUE
现在每个表单提交都必须由拾取表单的同一用户授权,这几乎击败了 CSRF。
放入令牌的另一件事是过期时间,这样一来,瞬间的客户端妥协或中间人攻击就不会泄漏将永远为该用户工作的令牌,并且值会发生变化密码重置,因此更改密码会使现有令牌无效。
【讨论】:
您可以简单地使用对当前会话甚至用户持久的令牌(例如用户密码哈希的哈希)并且无法由第三方确定(使用用户 IP 的哈希是不好的例如)。
然后您不必存储大量生成的令牌,除非会话过期(这可能需要用户再次登录),否则用户可以使用任意数量的选项卡。
【讨论】: