views:

1644

answers:

3

Django comes with CSRF protection middleware, which generates a unique per-session token for use in forms. It scans all incoming POST requests for the correct token, and rejects the request if the token is missing or invalid.

I'd like to use AJAX for some POST requests, but said requests don't have the CSRF token availabnle. The pages have no <form> elements to hook into and I'd rather not muddy up the markup inserting the token as a hidden value. I figure a good way to do this is to expose a vew like /get-csrf-token/ to return the user's token, relying on browser's cross-site scripting rules to prevent hostile sites from requesting it.

Is this a good idea? Are there better ways to protect against CSRF attacks while still allowing AJAX requests?

+3  A: 

If you know you're going to need the CSRF token for AJAX requests, you can always embed it in the HTML somewhere; then you can find it through Javascript by traversing the DOM. This way, you'll still have access to the token, but you're not exposing it via an API.

To put it another way: do it through Django's templates -- not through the URL dispatcher. It's much more secure this way.

bigmattyh
CSRF tokens should change with every operation. Are you suggesting that each operation update the token? Doesn't that mean that the user's operations will have to become synchronous (second can't start until the first has returned and given the new csrf token).
Mystic
This is not the best answer. As Carl Meyer says in his answer: there is no need for protecting AJAX requests from CSRF by using a token *in Django*, as Django already protects AJAX requests in a different way. This also removes Mystics concern.
hopla
A: 

Cancel that, I was wrong. (See comments.) You can prevent the exploit by ensuring your JSON follows the spec: Always make sure you return an object literal as the top-level object. (I can't guarantee there won't be further exploits. Imagine a browser providing access to the failed code in its window.onerror events!)

You can't rely on cross-site-scripting rules to keep AJAX responses private. For example, if you return the CSRF token as JSON, a malicious site could redefine the String or Array constructor and request the resource.

bigmattyh is correct: You need to embed the token somewhere in the markup. Alternatively, you could reject any POSTs that do have a referer that doesn't match. That way, only people with overzealous software firewalls will be vulnerable to CSRF.

phyzome
Ugh, that link again. At least the author has added a note at the end that he's wrong. JSON is in fact not vulnerable to redefining constructors, because the Javascript parser will raise an exception when you try to parse unadorned JSON.
John Millikin
+6  A: 

It's worth mentioning that protecting AJAX requests from CSRF is unnecessary, since browsers do not allow cross-site AJAX requests. In fact, the Django CSRF middleware now automatically exempts AJAX requests from CSRF token scanning.

This is only valid if you are actually checking the X-Requested-With header server-side for the "XMLHttpRequest" value (which Django does), and only exempting real AJAX requests from CSRF scanning.

Carl Meyer
This isn't true. An ajax request is obviously communicating with a server (yours). As an attacker, I can still create the same post requests from anywhere else via a number of different methods, and without CSRF protection, I will succeed if the user is logged in.
Nathan
@Nathan You cannot "create the same post requests from anywhere else via a number of different methods", because the browser will refuse to execute that post request (unless you can get my domain to serve your JS, in which case I obviously have another hole). You'd have to either bypass the same-domain restrictions, or trick a user's browser into doing a non-AJAX request and spoofing the X-Requested-With header. Either way, if you are able to do it that's a browser bug. Working demos welcome.
Carl Meyer
@Carl Meyer: I still disagree. I can (for example) lure a user to a website under my control which makes a post to the server in the same manner that the AJAX request does - no injection of javascript into your site necessary. There are no same-domain restrictions on an ordinary http post - I can post with any form to any domain. The cookies are sent with the request - not with the page. This still falls under the category of a Cross-Site-Request-Forgery.
Nathan
@Nathan Once again, no. You can get the user's browser to POST to my domain, sure, but if it is an ordinary form submission it will not be an AJAX request (per the X-Requested-With header that I am checking server-side). And you cannot force the user's browser to add that header without running afoul of same-domain AJAX restrictions. If you can, it's a browser bug. Again: if you believe this is possible, please construct a demo that shows it in operation.
Carl Meyer
@Carl Meyer - After reviewing your comments again and looking more carefully at the link you included in your answer, and I now agree with you. :-) You'll have to edit your answer before SO will let me change my vote. Thanks for bearing with me in this discussion, I learned some things.
Nathan
@Carl Meyer - As for the editing the answer, maybe just add that it is unnecessary because Django checks for the header - without the header checking, I believe CSRF would still be possible. Just to make the distinction that AJAX request in general could be vulnerable to CSRF, but are not in cases where the X-Requested-With header is checked, such as Django.
Nathan
Thanks @Nathan, that's a good edit suggestion; done.
Carl Meyer
Arrg. I wanted to change my downvote to an upvote, but apparently this isn't allowed if you first undo the downvote. http://meta.stackoverflow.com/questions/23147/change-a-vote-from-downvote-to-upvote-via-editing
Nathan
@Nathan thanks, but no worries :-)
Carl Meyer