在过去的几天里,我已经阅读了有关如何防止 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 的哈希是不好的) .
然后,您不必存储可能生成的大量令牌,除非会话到期(这可能需要用户再次登录),否则用户可以使用任意数量的选项卡。