Problem
I’m attempting to improve the security of the forms on my website. One of the forms makes use of AJAX, while the other is a simple “contact us” form. I’m attempting to include a CSRF token. The issue I’m encountering is that the token only appears in the HTML “value” on occasion. The value is empty the rest of the time. On the AJAX form, I’m using the following code:
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>
Any suggestions?
Asked by Ken
Solution #1
Please do not generate your tokens this way for security reasons: $token = md5(uniqid(rand(), TRUE), $token = $token = $token = $token = $token = $token = $token = $token
Try this out:
session_start();
if (empty($_SESSION['token'])) {
$_SESSION['token'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['token'];
An initiative to backport random bytes() and random int() into PHP 5 applications is one of my employer’s open source projects. It’s MIT licensed and available as paragonie/random compat on Github and Composer.
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'];
Use hash equals() instead of == or even === (PHP 5.6+ only; prior versions can use the hash-compat library).
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
}
}
By utilizing hash hmac, you can further restrict tokens to only be available for a specific form (). HMAC is a keyed hash function that can be used with weaker hash functions and is safe to use (e.g. MD5). Instead, I suggest using the SHA-2 family of hash functions.
First, generate a second token for use as an HMAC key, then use logic like this to render it:
<input type="hidden" name="token" value="<?php
echo hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
?>" />
After that, while checking the token, use a congruent operation:
$calc = hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
if (hash_equals($calc, $_POST['token'])) {
// Continue...
}
Without knowing $_SESSION[‘second token’], tokens generated for one form cannot be reused in another context. It’s critical to use a different HMAC key than the one you just dropped on the page as an HMAC key.
By adding this filter to their Twig environment, anyone who uses the Twig templating engine can benefit from a simpler dual strategy:
$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']);
}
)
);
You may use both the general purpose tokens and the Twig function in the following way:
<input type="hidden" name="token" value="{{ form_token() }}" />
Alternatively, there’s the locked-down version:
<input type="hidden" name="token" value="{{ form_token('/my_form.php') }}" />
Twig is solely concerned with template rendering; adequate token validation is still required. The Twig technique, in my opinion, provides more flexibility and simplicity while yet allowing for optimal security.
If you have a security requirement that each CSRF token can only be used once, the most straightforward solution is to regenerate it after each successful validation. However, doing so invalidates all prior tokens, which is inconvenient for users who use numerous tabs at the same time.
For these special instances, Paragon Initiative Enterprises maintains an Anti-CSRF library. It only works with one-time-use per-form tokens. When there are enough tokens in the session data (default configuration: 65535), the oldest unredeemed tokens will be cycled out first.
Answered by Scott Arciszewski
Solution #2
It appears that you require 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'];
}
Answered by datasage
Solution #3
When the variable $token is present in the session, it is not retrieved.
Answered by Dani
Post is based on https://stackoverflow.com/questions/6287903/how-to-properly-add-cross-site-request-forgery-csrf-token-using-php