125

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 :

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

Any suggestions?

Besworks
  • 4,123
  • 1
  • 18
  • 34
Ken
  • 3,091
  • 12
  • 42
  • 69
  • Just curious, what `token_time` is used for? – zerkms Jun 09 '11 at 04:02
  • @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. – Ken Jun 09 '11 at 04:09
  • 1
    @Ken: so user can get the case when he opened a form, post it and get invalid token? (since it has been invalidated) – zerkms Jun 09 '11 at 04:13
  • @zerkms: Thank you, but I'm a little confused. Any chance you could provide me with an example? – Ken Jun 09 '11 at 04:16
  • 2
    @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. – zerkms Jun 09 '11 at 04:18

4 Answers4

370

For security code, please don't generate your tokens this way: $token = md5(uniqid(rand(), TRUE));

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.

Shores
  • 95
  • 7
Scott Arciszewski
  • 33,610
  • 16
  • 89
  • 206
  • 1
    nice, but how to change the $token after user submitted the form? in your case, one token used for user session. – Akam Mar 01 '16 at 10:26
  • 2
    Look closely at how https://github.com/paragonie/anti-csrf is implemented. The tokens are single-use, but it stores multiple. – Scott Arciszewski Mar 01 '16 at 14:41
  • @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. – MNR Apr 20 '16 at 05:06
  • You mean something like [this, from the above anti-csrf library](https://github.com/paragonie/anti-csrf/blob/606274f8f8c6aa0a807e656d7fc0603b5c78fdac/src/AntiCSRF.php#L230-L247)? – Scott Arciszewski Apr 20 '16 at 05:15
  • 3
    I 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? – Hiroki Jun 05 '17 at 07:14
  • Yes. That's called failing closed. – Scott Arciszewski Jun 05 '17 at 15:31
  • but 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? – A Friend Nov 09 '17 at 14:02
  • 1
    Because 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. – Scott Arciszewski Nov 10 '17 at 22:44
  • For 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. – Geoffrey Apr 26 '18 at 08:57
  • I don't understand the use of token2 in the twig example. Btw: nice answer, i am using twig for a project – Luis Cabrera Benito Aug 08 '18 at 22:41
  • @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 Mar 01 '20 at 03:58
  • @ScottArciszewski - continuing from above - But since the SESSION is stored server side, there is no way the JavaScript form submission can tamper with that ? – MarcoZen Mar 01 '20 at 04:13
  • Is it important to change session's `SameSite` value to `Strict` or `Lax` 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. – ygngy Mar 23 '21 at 09:40
27

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'];
}
Community
  • 1
  • 1
datasage
  • 19,153
  • 2
  • 48
  • 54
3

The variable $token is not being retrieved from the session when it's in there

elitalon
  • 9,191
  • 10
  • 50
  • 86
Daniel
  • 30,896
  • 18
  • 85
  • 139
-1

You can use time() method with md5() to make it unique.

if (!isset($_SESSION['token'])) 
{
   $time = time();
   $_SESSION['token'] = md5($time);
   $_SESSION['token_time'] = $time;
}
else
{
   $token = $_SESSION['token'];
}
Md. Ziyed Uddin
  • 180
  • 1
  • 1
  • 10