Securing against Cross Site Request Forgery (CSRF) exploits in PHP
Cross Site Request Forgery (CSRF) is basically the opposite of an XSS exploit. Where XSS takes advantage of the user by means of a trusted web site, CSRF takes advantage of the web site by means of a trusted user.
An example of this is an attacker sending out fake emails with a link to delete a blog post, an email, whatever. The target user then clicks this and is taken to a delete page. Since the user is an administrator of this site, and they have a valid session, your web application goes ahead and deletes the record as requested. The user had no idea that’s what the link was taking them to and now their account has been deleted without their consent. Not cool.
This doesn’t have to be a text link either, it is often attached to an image or a button. This might sound like a small risk since most critical web site functions are behind forms that require POSTed data but this can just as easily be expanded upon to use a button or JavaScript to submit hidden forms.
How to protect against it
The first step is to ensure that no data altering actions are preformed by GET requests. Anything that performs an action on data requires a PUT, POST or DELETE request. A basic example of this is any insert, update or delete preformed on your database should be behind a POST form request. If the user clicks a delete button they should then be taken to a form where they submit this to verify the action. If data altering actions need to be preformed over GET, maybe for a RESTful API, then simply require a unique token in the query string, exactly how we will discuss it being in the hidden form data in the following examples.
Now that you are submitting forms for your data manipulations we will need to add CSRF tokens to our forms. Our CSRF token will be a standard Nonce (Number used Once). To do this we will generate a random token, store it in the user’s session, then add it as a hidden field to our form. Once this form is POSTed we can check the POSTed CSRF token against the one in the session.
First we’ll create a function to generate the token. This will usually be placed in a universally callable place, maybe as a route filter, voter, or a helper library.
1
2
3
4
5
6
7
8
9
10
11
12
//assuming the rest of the form class here
static function generateCsrf() {
$token = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);
Session::flash('csrfToken', $token);
return $token;
}
Note that we are using session flashdata here. This is supported concept in most session wrapping classes. Flashdata will be stored to the session but can only be accessed on one request and then it is destroyed. This keeps our token from being validate for more than this one request.
Next we’ll call this from our route and pass the token to the view that is generating the form
1
2
3
4
5
6
7
8
9
Route::get('/signup', function(){
$data['token'] = Form::generateCsrf();
return View::render('signup.form', $data);
});
And now for our view, “signup/form.php”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<form method="POST" action="/signup">
<label>
First Name:
<input type="text" name="first_name" />
</label>
<label>
Last Name:
<input type="text" name="last_name" />
</label>
<label>
Email:
<input type="text" name="email" />
</label>
<input type="hidden" name="token" value="<?=$token?>" />
<input type="submit" name="submit" value="Signup" />
</form>
When this form is POSTed we can now verify that the token is valid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Route::post('/signup', function(){
//this would probably be abstracted away into
//a route filter or your form validation
if ($_POST['token'] === Session::get('csrfToken')) {
//process the form
}
//like earlier, you should add a
//legit error message here
die('Invalid Form Data');
});
Now that this token checking is in place even if an attacker tricks a user into submitting a fake form they won’t have a matching CSRF token in their session data for your website so the request will fail.
Want to Learn More about PHP Security?
Buy the Book