How to properly add cross-site request forgery (CSRF) token using PHP
Solution 1
For security code, please don't generate your tokens this way: $token = md5(uniqid(rand(), TRUE));
rand()
is predictableuniqid()
only adds up to 29 bits of entropy-
md5()
doesn't add entropy, it just mixes it deterministically
Try this out:
Generating a CSRF Token
PHP 7
session_start();
if (empty($_SESSION['token'])) {
$_SESSION['token'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['token'];
Sidenote: One of my employer's open source projects is an initiative to backport random_bytes()
and random_int()
into PHP 5 projects. It's MIT licensed and available on Github and Composer as paragonie/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'];
Verifying the CSRF Token
Don't just use ==
or even ===
, use hash_equals()
(PHP 5.6+ only, but available to earlier versions with 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
}
}
Going Further with Per-Form Tokens
You can further restrict tokens to only be available for a particular form by using hash_hmac()
. HMAC is a particular keyed hash function that is safe to use, even with weaker hash functions (e.g. MD5). However, I recommend using the SHA-2 family of hash functions instead.
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']);
?>" />
And then using a congruent operation when verifying the token:
$calc = hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
if (hash_equals($calc, $_POST['token'])) {
// Continue...
}
The tokens generated for one form cannot be reused in another context without knowing $_SESSION['second_token']
. It is important that you use a separate token as an HMAC key than the one you just drop on the page.
Bonus: Hybrid Approach + Twig Integration
Anyone who uses the Twig templating engine can benefit from a simplified dual strategy by adding this filter to their Twig environment:
$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']);
}
)
);
With this Twig function, you can use both the general purpose tokens like so:
<input type="hidden" name="token" value="{{ form_token() }}" />
Or the locked down variant:
<input type="hidden" name="token" value="{{ form_token('/my_form.php') }}" />
Twig is only concerned with template rendering; you still must validate the tokens properly. In my opinion, the Twig strategy offers greater flexibility and simplicity, while maintaining the possibility for maximum security.
Single-Use CSRF Tokens
If you have a security requirement that each CSRF token is allowed to be usable exactly once, the simplest strategy regenerate it after each successful validation. However, doing so will invalidate every previous token which doesn't mix well with people who browse multiple tabs at once.
Paragon Initiative Enterprises maintains an Anti-CSRF library 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.
Solution 2
Security Warning:
md5(uniqid(rand(), TRUE))
is not a secure way to generate random numbers. See this answer 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'];
}
Solution 3
The variable $token
is not being retrieved from the session when it's in there
Related videos on Youtube
Ken
Updated on July 08, 2022Comments
-
Ken almost 2 years
I am trying to add some security to the forms on my website. One of the forms uses AJAX and the other is a straightforward "contact us" form. I'm trying to add a CSRF token. The problem I'm having is that the token is only showing up in the HTML "value" some of the time. The rest of the time, the value is empty. Here is the code I am using on the AJAX form:
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?
-
zerkms about 13 yearsJust curious, what
token_time
is used for? -
Ken about 13 years@zerkms I'm not currently using
token_time
. I was going to limit the time within which a token is valid, but have not yet fully implemented the code. For the sake of clarity, I've removed it from the question above. -
zerkms about 13 years@Ken: so user can get the case when he opened a form, post it and get invalid token? (since it has been invalidated)
-
Ken about 13 years@zerkms: Thank you, but I'm a little confused. Any chance you could provide me with an example?
-
zerkms about 13 years@Ken: sure. Let's suppose token expires at 10:00am. Now it is 09:59am. User opens a form and gets a token (which is still valid). Then user fills the form for 2 minutes, and sends it. As long as it is 10:01am now - token is being treated as invalid, thus user gets form error.
-
-
Scott Arciszewski over 8 yearsNote: I wouldn't trust
md5(uniqid(rand(), TRUE));
for security contexts. -
Akam over 8 yearsnice, but how to change the $token after user submitted the form? in your case, one token used for user session.
-
Scott Arciszewski over 8 yearsLook closely at how github.com/paragonie/anti-csrf is implemented. The tokens are single-use, but it stores multiple.
-
MNR about 8 years@ScottArciszewski What do you think about to generate a message digest from the session id with a secret and then compare the received CSRF token digest with again hashing the session id with my previous secret? I hope you understand what I mean.
-
Scott Arciszewski about 8 yearsYou mean something like this, from the above anti-csrf library?
-
Hiroki about 7 yearsI have a question about Verifying the CSRF Token. I if $_POST['token'] is empty, we shouldn't proceed, because the this post request was sent without the token, right?
-
Scott Arciszewski about 7 yearsYes. That's called failing closed.
-
A Friend over 6 yearsbut why does the token have to be a random value? surely it could just be a 1 or a 0 really because the session is local to that domain only?
-
Scott Arciszewski over 6 yearsBecause it's going to be echoed into the HTML form, and you want it to be unpredictable so attackers can't just forge it. You're really implementing challenge-response authentication here, not simply "yes this form is legit" because an attacker can just spoof that.
-
Geoffrey about 6 yearsFor single use tokens, rather then storing a history of tokens, you can just store a token in the client's browser in
localStorage
, which is shared between tabs, windows, etc. -
Luis Cabrera Benito almost 6 yearsI don't understand the use of token2 in the twig example. Btw: nice answer, i am using twig for a project
-
MarcoZen over 4 years@ScottArciszewski - just to clarify - since the value of the CSRF token can be seen in the html code, cant the attacker just use it directly in a spoofed form and submit via Javascript ? The POST value will be populated upon the JavaScript submission ?
-
MarcoZen over 4 years@ScottArciszewski - continuing from above - But since the SESSION is stored server side, there is no way the JavaScript form submission can tamper with that ?
-
ygngy over 3 yearsIs it important to change session's
SameSite
value toStrict
orLax
in your code to prevent CSRF? Because if you do not browser still will send your session cookie even from requests originated from other sites to your site. -
timhj about 3 yearsStraight up this is the correct answer to the guy's question. The other answer with 300+ upvotes solves a problem OP didn't ask about.