CSRF protection with 'self-validating' tokens
Cross-site Request Forgery is a very common social exploit method to make people unknowingly do things on their own behalf on a targeted website. It's the number four on the 2010 CWE/SANS Top 25 Most Dangerous Programming Errors list.
The main reason this problem exists in most websites is the fact that they don't check the origin of an incoming request that results in an action on that website. There are several ways a website can protect itself against these sort of attacks and I'm going to explain the way we, at Tweakers.net, have implemented our own protection method.
I'm not saying that Tweakers.net is 100% safe against CSRF attacks; I know we're not, and a recent topic on our forum made that somewhat painfully clear again: with the redesign of our account pages we separated certain data into several forms, but forgot to add our token-based security measure to each of them.
Adding a special token to an HTML form is a way to prevent these CSRF attacks. Such tokens can be used to verify whether the request to change data to your website actually originated from one of your own pages and thus may be considered 'valid'. We have been using our own token-system for quite some time and in various (but not all) places on our website, but our frontpage token-system suffered from some serious drawbacks.
On our forum we are using the user's session-id as a token for forms that can be used to manipulate data. This in itself is fine, but it means that session-id's are being exposed in the mark-up data, and as a token they are not actually limited to anything (except maybe on IP-address from the originating request).
When we designed our token system for the frontpage we wanted some more security; we wanted tokens that can only be used once for each request and that were tied to a user's session-id (or IP-address in case of not-logged-in users). So we simply set up a system that created random tokens and wrote them to the database with a creation-time and the user's session-id or IP-address. Tokens older than 24 hours were removed (and thus invalidated).
This has worked well for a long time, but performance-wise became a real bottleneck: the random insertions of tokens (the random token id being the primary key of the table) in a table having always around 500.000 'active' keys sometimes slowed down page-generation enormously.
When you have a table containing over half a million tokens of which only a very small percentage will actually be used you are most certainly doing something wrong, so we decided to change that. We only wanted to store tokens that where actually used to prevent re-use, and let all other other tokens expire by itself. The question was how to accomplish that without needing storage of those tokens. The answer was: creating tokens that can validate themselves against their use and expiration
The idea is simple: take those items that you want to be validated and hash them with a secret key. The result will be the token used in the form, and server-side we can recreate that same token and compare it to the value received from the request. That takes care of almost everything, except for the expiration part. We need to be able to determine the expiration 'plainly' from the token itself. Luckily, if you add the same expiration to the items that are being hashed, it is no problem to add it in a readable format to the token. Also, we can use this expiration as a timestamp to store the tokens that have been used, together with the actual token itself to create an unique key with a primary key that is not random but actual rather sequential. We added another 6-byte random identifier in order to create randomness for the same expiration timestamps.
So in short, this is what we do:
Here's a simplified version of our token-class that illustrated this method:
PHP:
To add a token to your HTML form, just add the output from
somewhere within the form itself, where [action] is an identifier to identify the current action (you can use the boolean 'true' to fall back to the current script's name). To validate, you can do something like this:
PHP:
You may use this code in any way you like, it's not licensed whatsoever. Please help make the web a safer place!
The main reason this problem exists in most websites is the fact that they don't check the origin of an incoming request that results in an action on that website. There are several ways a website can protect itself against these sort of attacks and I'm going to explain the way we, at Tweakers.net, have implemented our own protection method.
I'm not saying that Tweakers.net is 100% safe against CSRF attacks; I know we're not, and a recent topic on our forum made that somewhat painfully clear again: with the redesign of our account pages we separated certain data into several forms, but forgot to add our token-based security measure to each of them.
Adding a special token to an HTML form is a way to prevent these CSRF attacks. Such tokens can be used to verify whether the request to change data to your website actually originated from one of your own pages and thus may be considered 'valid'. We have been using our own token-system for quite some time and in various (but not all) places on our website, but our frontpage token-system suffered from some serious drawbacks.
On our forum we are using the user's session-id as a token for forms that can be used to manipulate data. This in itself is fine, but it means that session-id's are being exposed in the mark-up data, and as a token they are not actually limited to anything (except maybe on IP-address from the originating request).
When we designed our token system for the frontpage we wanted some more security; we wanted tokens that can only be used once for each request and that were tied to a user's session-id (or IP-address in case of not-logged-in users). So we simply set up a system that created random tokens and wrote them to the database with a creation-time and the user's session-id or IP-address. Tokens older than 24 hours were removed (and thus invalidated).
This has worked well for a long time, but performance-wise became a real bottleneck: the random insertions of tokens (the random token id being the primary key of the table) in a table having always around 500.000 'active' keys sometimes slowed down page-generation enormously.
When you have a table containing over half a million tokens of which only a very small percentage will actually be used you are most certainly doing something wrong, so we decided to change that. We only wanted to store tokens that where actually used to prevent re-use, and let all other other tokens expire by itself. The question was how to accomplish that without needing storage of those tokens. The answer was: creating tokens that can validate themselves against their use and expiration
The idea is simple: take those items that you want to be validated and hash them with a secret key. The result will be the token used in the form, and server-side we can recreate that same token and compare it to the value received from the request. That takes care of almost everything, except for the expiration part. We need to be able to determine the expiration 'plainly' from the token itself. Luckily, if you add the same expiration to the items that are being hashed, it is no problem to add it in a readable format to the token. Also, we can use this expiration as a timestamp to store the tokens that have been used, together with the actual token itself to create an unique key with a primary key that is not random but actual rather sequential. We added another 6-byte random identifier in order to create randomness for the same expiration timestamps.
So in short, this is what we do:
- determine expiration timestamp
- create a random 6-byte string
- hash together expiration timestamp + an action identifier (that binds the token to a certain action or page) + session-id or IP adress using a key consisting of the random 6-byte string and a secret key
- combine above 3 values into one single token
Here's a simplified version of our token-class that illustrated this method:
PHP:
| <?php
|
To add a token to your HTML form, just add the output from
FormToken::generateTokenHtml([action]);somewhere within the form itself, where [action] is an identifier to identify the current action (you can use the boolean 'true' to fall back to the current script's name). To validate, you can do something like this:
PHP:
| <?php
|
You may use this code in any way you like, it's not licensed whatsoever. Please help make the web a safer place!
04-'10 Tweakblogs: stats, quotes & performance
03-'10 Aftellen naar 1984 - deel 1
Comments
If a malicious individual gains enough control over a victims browser to POST requests, wouldn't they easily be able to do a GET before and obtain a perfectly valid token to do their nasty business with?
Nevermind, apparently GET requests have much better XSS protection than POST ones, which creates the need for developers to apply tricks like this one.
@Wouter: Nope, they don't have better protection (less actually), but the attacker doesn't get to see the responses. So you could trigger some requests, but since GET is idempotent it doesn't hurt anybody.
And beware, XSS != CSRF
And beware, XSS != CSRF
Wouter: if the browsersecurity is indeed that much breached, it is quite hard to do anything about it. If you have enough control to do a GET-request, analyze the page contents and then a POST-request, there is simply no way of distinguishing it from actual usage.
This is not the area of security issues we're protecting against with these tokens. But if someone constructs a POST-form (for instance with javascript) and submits "some" action to your site, you can prevent that since the POST-request will lack a valid token for the specific browser/user.
(its somewhat ironical that a spam-reaction occured here..., lets remove that one)
This is not the area of security issues we're protecting against with these tokens. But if someone constructs a POST-form (for instance with javascript) and submits "some" action to your site, you can prevent that since the POST-request will lack a valid token for the specific browser/user.
(its somewhat ironical that a spam-reaction occured here..., lets remove that one)
[Comment edited on Saturday 17 April 2010 10:51]
No Wouter, you're wrong. The "problem" is that with a CSRF attack, an attacker can only let the victims submit a single request. They don't get the "return page" that the victim receives. So the attacker is pretty much blind. This is exploitable in combination with another XSS exploit which makes it possible to return information to the attacker, but by itself this is an excellent method.
@crisp, all of this reminds me a bit of Kerberos. It might be interesting for you to have a look at that. It may inspire you with some new ideas.
@crisp, all of this reminds me a bit of Kerberos. It might be interesting for you to have a look at that. It may inspire you with some new ideas.
In a 'Safe Software' course I took at university last semester we spent some time discussing CSRF. The course teachers also developed a Firefox plugin that provides some extra protection from CSRF by stripping HTTP authentication headers and session cookies from cross site requests. Unfortunately, this also causes some useability issues, certainly in a web2.0 context.
Ofcourse, preventing CSRF is ultimately the responsability of the web site owners, not the users. But unfortunately we all know that not all web applications are as secured as they should be...
The link for those interested: https://addons.mozilla.org/nl/firefox/addon/58189
Ofcourse, preventing CSRF is ultimately the responsability of the web site owners, not the users. But unfortunately we all know that not all web applications are as secured as they should be...
The link for those interested: https://addons.mozilla.org/nl/firefox/addon/58189
Hehe, three people explaining the same thing within a few minutes, I guess I didn't word my second post very well
.
The attacker being "blind" is what I meant by XSS protection against GET requests, as in: something the browser does to keep me safe. Someone can always request a page as if they were me, but unless there are serious exploits in the browser, or the server software is not so "idempotent" as it should be, that will not matter.
But apparently there is no such protection that the browser gives me against POST. Would it not be pretty straightforward to check form targets against the same origin policy and get rid of this CSRF problem? I imagine this change would break at least some legacy applications, but they could at least add a warning, or perhaps make the switch for HTML5 mode only.
The attacker being "blind" is what I meant by XSS protection against GET requests, as in: something the browser does to keep me safe. Someone can always request a page as if they were me, but unless there are serious exploits in the browser, or the server software is not so "idempotent" as it should be, that will not matter.
But apparently there is no such protection that the browser gives me against POST. Would it not be pretty straightforward to check form targets against the same origin policy and get rid of this CSRF problem? I imagine this change would break at least some legacy applications, but they could at least add a warning, or perhaps make the switch for HTML5 mode only.
Crisp, first of all a great blogpost! But I want to notice you, there is a small error in your code. The function "getRandomHexString($byteCount)" will return a string with twice the specified length, because there is a concatenation on two random chars in the main loop. I think you meant to append a single random char instead of two chars.
PHP code:
while ($byteCount--)
$byteString .= $hexstring[mt_rand(0, 15)] . $hexstring[mt_rand(0, 15)];
PHP code:
while ($byteCount--)
$byteString .= $hexstring[mt_rand(0, 15)] . $hexstring[mt_rand(0, 15)];
@DuoCoding: no, the function is correct; 6 bytes hex-encoded will give you a string of 12 characters.
However, I can see how this function is confusing so I replaced it with a simpler (and slightly faster) function (at Tweakers.net we read from /dev/urandom, but since that's not supported cross platform I wrote this drop-in replacement function).
However, I can see how this function is confusing so I replaced it with a simpler (and slightly faster) function (at Tweakers.net we read from /dev/urandom, but since that's not supported cross platform I wrote this drop-in replacement function).
Nice information, I really appreciate the way you presented.Thanks for sharing..
Terwijl ik een comment tik en submit krijg ik "The security token is invalid", blijkbaar is het niet 'voutloos' dus. Net moest ik gegevens invoeren en nu ben ik ineens ingelogd met m'n GoT-account. Een bugje zit in een klein hoekje.
Maar verder: mooi artikel, hopelijk nemen velen dit goede idee over.
Maar verder: mooi artikel, hopelijk nemen velen dit goede idee over.
Dat je token dan niet meer geldig is is een oorzaak van het uitgelogd zijn; je sessie komt dan immers niet meer overeen en dat is een onderdeel van het token. Wat dan weer de oorzaak is van het uitgelogd zijn is dan nog de vraag; dat kan meerdere oorzaken hebben, en meestal duidt dat op een clientside issue...Cartman! wrote on Saturday 31 July 2010 @ 23:11:
Terwijl ik een comment tik en submit krijg ik "The security token is invalid", blijkbaar is het niet 'voutloos' dus. Net moest ik gegevens invoeren en nu ben ik ineens ingelogd met m'n GoT-account. Een bugje zit in een klein hoekje.
Daarvoor heb ik gewoon op GoT rondgeneusd en gepost. Ik volgde het linkje in PRG en kwam hier. Onderaan werd niet gedetecteerd dat ik ingelogd was want ik moest een hele zwik gegevens invoeren. Toen ik m'n reply wilde posten kreeg ik dus de foutmelding. Toen kreeg ik ineens "Your comment will be posted under Cartman!" etc. Dat om die reden de token dus niet werkte is logisch, het formulier maakte blijkbaar een token zonder mijn sessieId maar met mijn ip waarna ie faalde. Maakt niet uit verder maar ergens zat er dus iets niet lekkercrisp wrote on Monday 02 August 2010 @ 09:07:
[...]
Dat je token dan niet meer geldig is is een oorzaak van het uitgelogd zijn; je sessie komt dan immers niet meer overeen en dat is een onderdeel van het token. Wat dan weer de oorzaak is van het uitgelogd zijn is dan nog de vraag; dat kan meerdere oorzaken hebben, en meestal duidt dat op een clientside issue...
Ik vraag me af of je add_rewrite_var (PHP) zou kunnen gebruiken om automatisch dit soort dingen toe te voegen aan een form. Ook de controle zou dan automatisch kunnen gebeuren in een frameworkje.
Comments are closed