How to Properly Add Cross-site Request Forgery (Csrf) Token Using PHP

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

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

  • rand() is predictable
  • uniqid() 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.

How to prevent Cross-site request forgery (CSRF) effectively in PHP

Is this a good way to prevent CSRF?

Yes. What this does is to force the client to do a GET on the form before it can do a POST to your form handler. This prevents CSRF in modern browsers since browsers will prevent client-side Javascript to do an XHR GET request to a foreign domain, so a 3rd party cannot imitate your form on their site and successfully get a valid token for the submission.

When another page is opened as well that sets the same $_SESSION variable, the previous (still open) page becomes invalid, how to prevent this?

Allow several tokens to be valid at a time, keeping an array of valid tokens in the session. Alternatively, store no tokens at all and use a token signing scheme instead. I've dabbled in and explained that here. Alternative 2: just use a single token for the whole session, without invalidating tokens. (tip o' the hat to @SilverlightFox in the comments)

For forms this method is clear, but how to handle normal links? Is it necessary to append the token to each link as well?

No. You only need to protect POST requests since presumably only POST requests can alter sensitive data (wink wink nudge nudge, you're sticking to REST conventions, right?!) and XHR GET requests are already blocked browser-side.

Forging a Cross Site Request Forgery (CSRF) token

You might misunderstand what this protects against, so let's first clarify what CSRF is, and what it is not. Sorry if this is not the point of confusion, might still be helpful for others, and we will get to the point afterwards.

Let's say you have an application that allows you to say transfer money with a POST request (do something that "changes state"), and uses cookie-based sessions. (Note that this is not the only case csrf might be possible, but by far the most common.) This application receives the request and performs the action. As an attacker, I can set up another application on a different domain, and get a user to visit my rogue application. It does not even have to look similar to the real one, it can be completely different, just having a user visit my rogue domain is enough. I as the attacker can then send a post to the victim application's domain, to the exact url with all the necessary parameters so that money gets transferred (the action will be performed). The victim user need not even know if this happens in xhr from javascript - or I can just properly post a form, the user gets redirected, but the harm is done.

This is affected by a few things, but the point is that cross-origin requests are not prevented by the same origin policy, only the response will not be available to the other domain - but in this case when server state changes in the victim application (like money gets transferred), the attacker might not care much about the response itself. All this needs is that the victim user that visits the attacker's page while still being logged in to the victim application. Cookies will be sent with the request regardless of the page the request is sent from, the only thing that counts is the destination domain (well, unless samesite is set for the cookie, but that's a different story).

Ok, so how does Rails (and similar synchronizer token solutions) prevent this? If you lok at lines 318 and 322 in the source, the token received from the user is compared to the one already stored in the session. When a user logs in, a random token is generated and stored for that particular user, for that particular session. Subsequent requests that change state (everything apart from GET) check if the token received from the client is the same that's stored in the session. If you (or an attcker) generate and send a new one, that will be different and the request will fail validation. An attacker on their own website cannot guess the correct token to send, so the attack above becomes impossible. An attacker on a different origin also cannot read the token of a user, because that is prevented by the same origin policy (the attacker can send a GET request to the victim app, but cannot read the response).

So to clarify, CSRF is not a protection against parameter tampering, which might have caused your confusion. In your own requests, if you know the token, you can change the request in any way, send any parameter, the CSRF token does not protect against this. It is against the attack outlined above.

Note that the description above is only scratching the surface, there is a lot of depth to CSRF protection, and Rails too does a little more, with some other frameworks doing a lot more to protect against less likely attacks.

CSRF (Cross-site request forgery) attack example and prevention in PHP

This could become an example of CSRF if :

  • that link is fetched (via an <img> tag, for example) : forgery
  • from another site : cross-site



For example, if I could inject this <img> tag in the HTML source-code of stackoverflow (and I can, as stackoverflow allows one to use <img> tags in his posts) :

<img src="http://mysite.com/vote/30" />

You would just have voted for that item ;-)



The solution that is generally used is to place a token, that has a limited life-time, in the URL, and, when the URL is fetched, check that this token is still valid.

The basic idea would be :

  • When generating the page :

    • generate a unique token
    • store it in the user's session
    • and place it in the links of the page -- which would look like this : http://mysite.com/vote/30?token=AZERTYUHQNWGST
  • When the voting page is called :

    • Check if the token is present in the URL
    • Check if it's present in the user's session
    • If not => do not register the vote

The idea there is :

  • Tokens don't have a long life-time, and are hard to guess
  • Which means your attacker :

    • has only a window of a few minutes during which his injection will be valid
    • will have to be good at guessing ^^
    • will have to generate a different page for each user.



Also, note that the shorter the user's session remains active after he has left your site, the less risks there are that it's still valid when he visits the bad website.

But here, you have to choose between security and user-friendly...



Another idea (that's not perfectly secure, but helps against guys would don't know how to force a POST request), would be to only accept POST requests when people are voting :

  • The browser is sending GET requests for injected tags
  • As this URL is modifying some data, anyway, it should not work with GET, but only with POST

But note that this is not perfectly safe : it's (probably ? ) possible to force/forge a POST request, with some bit of Javascript.

PHP Safe way to put URL in form to protect against from xss / cross site request forgery CSRF attack

For CSRF Protection --

  • Use csrf token, now what is csrf token it is nothing but a unique value which will be generated by your application for each form submission event and attache to each and every form as input hidden value.
    and at the same time application need to set that csrf token value into a SESSION so that at the time of form submission it can check the token value is valid or not. This is one way you can protect CSRF attack.

For XSS Protection --

  • 1st thing is set form validation both front end and back end
  • you can use different php filter methods
  • Use htmlspecialchars() which Convert special characters to HTML entities at the time of retrieving the data or showing the data

Cross-Site Forgeries (CSRF) and Public Application Interface

You don't need to worry about CSRF if it the request does only trivial updates (more on definition of "trivial updates" below). In a typical CSRF attack you have:

  1. User logged into website A with cookies for website.
  2. In same browser session, user accesses website B.
  3. Website B causes browser to access some URL on website A.
  4. Website A responds to the request thinking it came from the user.

Because of the same origin policy, website B can't see the response of the access; it can only cause the access to occur. So if the request only does trivial updates, the user's browser will display the content from site A when they are accessing site B, but we know that the user is entitled to see that data anyway because site A would have rejected the request otherwise.

The user may freak out a bit, call support of website A, whatever, but there was no data breach.

If the requests does more than trivial updates, then website B has caused the user to take an action on site A without that being their intent. Very bad for user and site A.

So trivial updates only is safe without CSRF protection. Everything else needs protection.

"Trivial updates" will mean different things for different sites. For example, updating someone's bank balance is clearly not trivial. But is logging a get balance request in a database a trivial update? How about performing a time-consuming query? Your site's technical and business model will determine what you consider trivial updates and acceptable to leave unprotected from CSRF and what you feel must be protected.

In your model of app-authentication but not user authentication, you don't need to worry about CSRF as all accesses are public.

One final note, it is frequently a good idea to add no CSRF protection on the logout functionality. This helps to ensure that the user can always logout, even if a bug happens in CSRF validation. That said, the risk of this is that site B can force a logout of the user from site A. You have to pick what makes the most sense for you.



Related Topics



Leave a reply



Submit