I'm working on a completely ajax-driven application where all requests pass through what basically amounts to a main controller which, at its bare bones, looks something like this:
if(strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') { fetch($page); }
Is this generally sufficient to protect against cross-site request forgeries?
It's rather inconvenient to have a rotating token when the entire page isn't refreshed with each request.
I suppose I could pass and update unique token as a global javascript variable with every request -- but somehow that feels clumsy and seems inherently unsafe anyway.
EDIT - Perhaps a static token, like the user's UUID, would be better than nothing?
EDIT #2 - As The Rook pointed out, this might be a hair-splitting question. I've read speculation both ways and heard distant whispers about older versions of flash being exploitable for this kind of shenanigans. Since I know nothing about that, I'm putting up a bounty for anyone who can explain how this is a CSRF risk. Otherwise, I'm giving it to Artefacto. Thanks.
The X-Requested-With is a request header that a user agent may use to store information about the creation of the request such as client information, method used. Note that the X-Requested-With cannot be added to a cross domain request without the consent of the server via CORS.
XMLHttpRequest (XHR) objects are used to interact with servers. You can retrieve data from a URL without having to do a full page refresh. This enables a Web page to update just part of a page without disrupting what the user is doing. XMLHttpRequest is used heavily in AJAX programming.
I'd say it's enough. If cross-domain requests were permitted, you'd be doomed anyway because the attacker could use Javascript to fetch the CSRF token and use it in the forged request.
A static token is not a great idea. The token should be generated at least once per session.
EDIT2 Mike is not right after all, sorry. I hadn't read the page I linked to properly. It says:
A simple cross-site request is one that: [...] Does not set custom headers with the HTTP Request (such as X-Modified, etc.)
Therefore, if you set X-Requested-With
, the request has to be pre-flown, and unless you respond to pre-flight OPTIONS
request authorizing the cross-site request, it won't get through.
EDIT Mike is right, as of Firefox 3.5, cross-site XMLHttpRequests are permitted. Consequently, you also have to check if the Origin
header, when it exists, matches your site.
if (array_key_exists('HTTP_ORIGIN', $_SERVER)) { if (preg_match('#^https?://myserver.com$#', $_SERVER['HTTP_ORIGIN']) doStuff(); } elseif (array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER) && (strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest')) doStuff();
I do not believe that this is safe. The same origin policies are designed to prevent the documents from different domains from accessing the content that is returned from a different domain. This is why XSRF problems exist in the first place. In general XSRF doesn't care about the response. It is used to execute a specific type of request, like a delete action. In the simplest form, this can be done with a properly formatted img tag. Your proposed solution would prevent this simplest form, but doesn't protect someone from using the XMLHttp object to make the request. You need to use the standard prevention techniques for XSRF. I like to generate a random number in javascript and add it to the cookie and a form variable. This makes sure that the code can also write cookies for that domain. If you want more information please see this entry.
Also, to pre-empt the comments about XMLHttp not working in script. I used the following code with firefox 3.5 to make a request to google from html running in the localhost domain. The content will not be returned, but using firebug, you can see that the request is made.
<script> var xmlhttp = false; if (!xmlhttp && typeof XMLHttpRequest != 'undefined') { try { xmlhttp = new XMLHttpRequest(); } catch (e) { xmlhttp = false; } } if (!xmlhttp && window.createRequest) { try { xmlhttp = window.createRequest(); } catch (e) { xmlhttp = false; } } xmlhttp.open("GET", "http://www.google.com", true); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == 4) { alert("Got Response"); alert(xmlhttp.responseText) } } xmlhttp.send(null) alert("test Complete");
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With