Stomping CSRF attacks

Cross-site request forgery (CSRF) attacks have gained increasing notoriety as websites have grown increasingly dependent on each other for retrieving, delivering, and managing information. Though still less prominent than well-known cross-site scripting (XSS) and SQL injection attacks, they nevertheless pose a considerable risk to internet users. To understand the basic attack, consider an innocuous link in the inbox module of a web application:

<a href="/inbox?action=delete&id=144">delete</a>

There isn’t a problem if the request originates from the inbox itself, but imagine the following scenario:

Hell-bent on destruction, little Bobby Blackhat copies the URL used to delete messages from the inbox and pastes it as a link into a carefully forged e-mail from a Nigerian prince. When the link is clicked by a user who is logged into the application, the application sees the user’s valid login and performs exactly the way it was designed.

Our user has just fallen victim to a CSRF. The attack is obvious: instead of the deletion request being POSTed and subject to any validation techniques used to protect the application’s form processor, it’s being passed on to the server by way of a url-encoded GET request that anyone with a rudimentary knowledge of HTML can forge.

Enter nonce

The solution, then, is to set up our application so that it won’t process illegitimate requests. The idea is to put just enough secret sauce on our requests to make that malicious users won’t be easily be able to forge them. Broadly, the process follows three steps:

  1. A unique single-use token is generated by the server and provided to a single user
  2. Legitimate users include the token in any requests they make to the server
  3. Requests that do not include a valid token are discarded as forgeries

The token in question is known as a number-used-once—an nonce. In its most basic form, an nonce will include an action, timestamp, and some piece of information unique to each user. These attributes are typically hashed together into a url-safe code that authorizes the user to perform a specific action within a specific window of time.

The beauty of the nonce is that verification only requires the server to regenerate the nonce. If the version created by the server matches the version created by the user, the nonce is approved and the action go forward. Taken together, the functions involved look a little like this:

function create_nonce( $action ) {

  global $timeout, $user_id;

  $time = ceil( time() / ( 3600 * $timeout ) );
  return sha1( "$time:$user_id:$action" );

function verify_nonce( $nonce, $action ) {

  return ( create_nonce( $action ) == $nonce );

Returning to our original example, we would now bolster any deletions requests with the addition of our nonce (truncated for the sake of demonstration):

<a href="/inbox?action=delete&id=144<strong>&nonce=ebc5c3ebb1f18</strong>">delete</a>

Now, when the link is clicked, our application would be able to verify the request’s authenticity by calling the verify_nonce function with both the action and the nonce specified.

<?php if( !verify_nonce( $_GET[ 'nonce' ], $_GET[ 'action' ] ) ) 
    die('action unavailable'); ?>

Everything tastes better with salt

The trouble with the nonces outlined thus far is that they can still be recreated by anyone with knowledge of the format, content, and hashing function used to create them in the first place. Though it has become much harder to launch a basic CSRF attack against our application, we are by no means invulnerable. Anyone looking through our code on GitHub, for instance, can quickly acquire the information necessary to replicate an nonce that will be valid for at least some window of time.

Our best defense against this kind of attach is to privatize our nonces by including a secret known only to our application in each hash we create:

function create_nonce( $action ) {

    global $timeout, $user_id;

    <strong>$my_application_secret = '8d41b6e08965ebc5c3ebb1f18c012969';</strong>

    $time = ceil( time() / ( 3600 * $timeout ) );
    return md5( <strong>$my_application_secret .</strong> "$time:$user_id:$action" );

To replicate our nonces, an attacker would now need to have knowledge of our application secret — known as a salt — and include it in every request they forge. As long as it remains a secret, the forgers task just got significantly harder.

While this scenario focuses specifically on GET request attacks, similar techniques may be used to deny access to other inappropriate HTTP requests. An nonce stored in a hidden form field, for instance, may be used to verify the intent of any user-submitted data.

A cautionary note

All that being said, it’s important to remember that nonces are only a single tool in the toolbox of secure development. They won’t magically guarantee security if sprinkled haphazard around an application, but if used correctly they can help make a project less insecure.

Let’s keep in touch

Reach out on Twitter or subscribe for (very) occasional updates.

Hey, I'm RJ: digital entomologist and intermittent micropoet, writing from the beautiful Rose City.