Advanced CSRF Usage in PHP
This expands on my post, Securing against Cross Site Request Forgery (CSRF) exploits in PHP. In the previous post we discussed the basics of how to protect against CSRF exploits. I recommend reading that if you haven’t yet. Now let’s cover a more advanced use case.
Multiple CSRF Tokens
The previous solution only covered basic, one time use protection but what if you need to allow users to use multiple tabs or protect AJAX calls in addition to regular ol’ forms. You don’t want a user’s form token to expire simply because an AJAX call fired off in the background.
The solution is to support multiple CSRF tokens at once.
Here was our old code to generate a token
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;
}
and here is our new function
1
2
3
4
5
6
7
8
9
10
11
//assuming the rest of the form class here
static function generateCsrf() {
$token = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);
$_SESSION['csrfTokens'][$token] = time();
return $token;
}
What this does is instead of storing the token as the value we are storing an array of tokens and then their corresponding creation timestamps. The previous example used the Laravel Session class but in this example I’m using standard PHP session handling since I don’t want this to be confusing.
Now the validation of the CSRF tokens looks like this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Route::post('/signup', function(){
//this would probably be abstracted away into
//a route filter or your form validation
//here we are setting our expiration timestamp
//to 1 day ago
$expiration = time() - 86400; //86400 is the number of seconds in 24 hours
if (
isset($_SESSION['csrfTokens'][$_POST['token']]) &&
$_SESSION['csrfTokens'][$_POST['token']] >= $expiration
) {
//process the form
}
//like earlier, you should add a
//legit error message here
die('Invalid Form Data');
});
This new function not only checks that the token exists but also checks that it hasn’t expired.
Garbage Collection
You really should use a Session class to wrap these calls to the $_SESSION data and then write a garage collection routine to clear expired tokens. Here is an example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Session
{
function validateCSRFToken($token) {
$this->garbageCollectCsrfTokens();
//if it is a validate token clear it
//and then return true
if (isset($_SESSION['csrfTokens'][$_POST['token']]) === TRUE) {
unset($_SESSION['csrfTokens'][$_POST['token']]);
return TRUE;
}
return FALSE;
}
function garbageCollectCsrfTokens() {
//here we are setting our expiration timestamp
//to 1 day ago
//this has been hardcoded here for simplicity
$expiration = time() - 86400; //86400 is the # of seconds in 24 hours
foreach ($_SESSION['csrfTokens'] as $k => $time) {
if ($time < $expiration) {
//unset since it's expired
unset($_SESSION['csrfTokens'][$k]);
}
}
}
}
Everytime the token is evaluated a garabage collection routine will be ran against all the tokens. The route logic to check the tokens has now been simplified to this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Route::post('/signup', function(Session $session){
//this would probably be abstracted away into
//a route filter or your form validation
if ($session->validateCSRFToken($_POST['token']) === TRUE) {
//process the form
}
//like earlier, you should add a
//legit error message here
die('Invalid Form Data');
});
You will now be able to use multiple CSRF tokens at once in the same user session. Plus have the added protection of expiring tokens.
Want to Learn More about PHP Security?
Buy the Book