How to properly add cross-site request forgery (CSRF) token using PHP

159,954

Solution 1

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.

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

Share:
159,954

Related videos on Youtube

Ken
Author by

Ken

Updated on July 08, 2022

Comments

  • Ken
    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
      zerkms about 13 years
      Just curious, what token_time is used for?
    • Ken
      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
      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
      Ken about 13 years
      @zerkms: Thank you, but I'm a little confused. Any chance you could provide me with an example?
    • zerkms
      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
    Scott Arciszewski over 8 years
    Note: I wouldn't trust md5(uniqid(rand(), TRUE)); for security contexts.
  • Akam
    Akam over 8 years
    nice, but how to change the $token after user submitted the form? in your case, one token used for user session.
  • Scott Arciszewski
    Scott Arciszewski over 8 years
    Look closely at how github.com/paragonie/anti-csrf is implemented. The tokens are single-use, but it stores multiple.
  • MNR
    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
    Scott Arciszewski about 8 years
    You mean something like this, from the above anti-csrf library?
  • Hiroki
    Hiroki about 7 years
    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?
  • Scott Arciszewski
    Scott Arciszewski about 7 years
    Yes. That's called failing closed.
  • A Friend
    A Friend over 6 years
    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?
  • Scott Arciszewski
    Scott Arciszewski over 6 years
    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.
  • Geoffrey
    Geoffrey about 6 years
    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.
  • Luis Cabrera Benito
    Luis Cabrera Benito almost 6 years
    I don't understand the use of token2 in the twig example. Btw: nice answer, i am using twig for a project
  • MarcoZen
    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
    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
    ygngy over 3 years
    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.
  • timhj
    timhj about 3 years
    Straight 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.