views:

250

answers:

5

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.

A: 

I do not think this offers any kind of protection. An attacking site could still use xmlhttprequest for its cross-site request bypass your check.

Alex
Would you happen to know a proper solution? You don't have to hold my hand and walk me through it, but perhaps you could point me in the right direction? :)
Greg
@Greg: No not really. I was thinking using a one-time token is the way to go. But I am not experienced with that.
Alex
+1  A: 

Short answer : no. Any attacker would just use Ajax himself to attack your website. You should generate a random token with a short but not too much lifetime which you would update during each ajax request.

You'd have to use an array of tokens in javascript as you may have multiple ajax request running at the same time.

Arkh
I was under the impression that cross-domain AJAX requests are not permitted? I guess there are workarounds?
Greg
The attacker can use Firebug or an another developer console to AJAX in-domain.
SHiNKiROU
-1, care to write an exploit to backup that comment? No? I didn't think so. xmlhttprequest cannot be cross site, its a violation of the same origin policy. it might not be a strong method of defiance, but you can't write an exploit for this. Read the google browser security handbook.
Rook
"xmlhttprequest cannot be cross site", that's client side. If your user's browser has some problem, why not do what you can on the server side ?Flash is not your friend either. And last but not least : you may also have some XSS vulnerable page and it may be easier to make your client send requests than steal his credentials.
Arkh
+4  A: 

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(); 
Artefacto
To the gentleman who down voted me: can you explain what's wrong with my answer?
Artefacto
It's a bad answer because just adding something to the header offers absolutely no protection because it can easily be forged. See the accepted answer for a good solution to the problem.
musicfreak
@musicfreak This header is added only when the request is made through Javascript. CSRF cannot be accomplished with Javascript due to cross-site restrictions. Consequently, it's enough. Sure you could "forge" the header, e.g. by building a browser that would always send this header. But by doing it, you'd shooting yourself in the foot. Protection against CRFS is protection for the users **from themselves**, against requests that were maliciously and programatically made by themselves.
Artefacto
@musicfreak This is also a relatively known method. See http://code.djangoproject.com/ticket/8127 "Assuming this is a check for the X-Requested-By: XMLHttpRequest header, I'm all for it - it's a nice, secure way of allowing Ajax requests to avoid having to include a form token, which can be tricky to make available to the Ajax code otherwise."
Artefacto
+1 you are correct sir. Don't mind these other fools, for they do not know what they are talking about. Have you read the google browser sec handbook? I think you have.
Rook
My mistake, I did not fully read the OP's question. If you edit your answer I will remove the downvote. :) @The Rook: I understand that my comment was not accurate under the given circumstances but I don't think that warranted a personal attack. I could say the same about you for some of the answers you've posted on SO as well.
musicfreak
@musicfreak Ok I'll delete my comments, I apologize, and I'll avoid this in the future. You down-voted a perfectly good answer because you do not understand and i was trying to defend this answer. You should read the Google browser sec handbook in its entirety. Maybe write a CSRF exploit while your at it :). Then you can see that headers really can't be forged in this attack. TamperData rocks, but it can't be used in CSRF. I guess this is an easy oversight to make.
Rook
@musicfreak I admit, I'm an asshole. I down vote people that are wrong and I chew people out for making mistakes. When in all reality this is a very complex topic and its difficult to keep all the rules straight. Its also upsetting to see the wrong answer get selected, especially when it comes to security because someone can get really hurt. (also you have to edit the post before you can remove the -1 :)
Rook
Downvote removed. @The Rook: It's all good. It was my fault for not fully reading the OP's question, so I guess I deserve to get chewed out, just not that harshly. :P I understand the topic (maybe not as well as you, but I do understand it), it was just a misunderstanding of the question on my part.
musicfreak
(-1) I'm pretty sure that this is indeed an issue. Please see my answer for a full explanation as to why I think this. If I am wrong, I would love to hear how.
Mike
@Mike Nice catch! This is indeed a feature that was added in Firefox 3.5. See my edit.
Artefacto
@Mike OK another edit. The problem with your example is that you did not set the HTTP_X_REQUESTED_WITH header.
Artefacto
@Artifacto You are right, in FF 3.5 and Safari 5, the browser will make an options request allowing the server to respond before allowing a custom header to be set. In IE 6, I get a security warning, but if the user selects yes, then the request is made with the custom header. I didn't test non-js runtimes like flash and java.It still seems to me like leaving this up to the browser implementation of XMLHttp is a little risky and I wonder what older browsers would do. The standard mechanisms referred to in my answer will work in all browsers/runtimes.
Mike
I undid the -1. I just did that to get your attention :)
Mike
@Mike You are already leaving this to the browser implementation. The only difference is whether you're relying that browser doesn't send the request through at all or whether you're relying that you can't read the response back. I don't have IE6 to test, so I'll have to believe you on that.
Artefacto
@Mike By the way, does IE6 allow you to read the response back? Because if it does, there's nothing you can do anyway.
Artefacto
A: 

What you are doing is secure because xmlhttprequest is usually not vulnerable to cross-site request forgery.

As this is a client side problem, the safest way would be to check the security architecture of each browser :-)

(This is a summary; I am adding this answer because this question is very confusing, let's see what the votes say)

Alex
+1  A: 

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");

Mike
sorry I had to edit it to able to remove my upvote.
Artefacto
I'm giving it to Artefacto, because it looks like X-Requested-With doesn't make it through, but this was interesting. Thank you.
Greg