如何使用 PHP 正确添加跨站请求伪造(CSRF)令牌

我试图添加一些安全的形式在我的网站上。其中一个表单使用 AJAX,另一个是直接的“联系我们”表单。我想加一个 CSRF 令牌。我遇到的问题是,令牌只在某些时候显示在 HTML“ value”中。其余时间,该值为空。下面是我在 AJAX 表单中使用的代码:

PHP:

if (!isset($_SESSION)) {
session_start();
$_SESSION['formStarted'] = true;
}


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

HTML:

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

有什么建议吗?

166515 次浏览

当变量 $token在会话中时,不会从会话中检索它

Security Warning: md5(uniqid(rand(), TRUE)) is not a secure way to generate random numbers. See 这个答案 for more information and a solution that leverages a cryptographically secure random number generator.

Looks like you need an else with your if.

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

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

Try this out:

生成 CSRF 令牌

PHP 7

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

旁注: 我雇主的开源项目中的一个是将 random_bytes()random_int()支持到 PHP5项目的倡议。它是麻省理工学院授权的,在 Github 和 Composer 上以 Pargonie/Random _ compat的形式提供。

PHP 5.3+ (or with 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 + ,但可以在 大杂烩库的早期版本中使用)。

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
}
}

进一步使用每表单令牌

可以使用 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 环境中,从简化的双重策略中获益:

$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 策略提供了更大的灵活性和简单性,同时保持了最大化安全性的可能性。


Single-Use CSRF Tokens

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

Paragon Initiative Enterprises maintains an 反 CSRF 库 for these corner cases. It works with one-use per-form tokens, exclusively. When enough tokens are stored in the session data (default configuration: 65535), it will cycle out the oldest unredeemed tokens first.

可以在 md5()中使用 time ()方法来使其惟一。

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