Notes on CSRF and JSON APIs

This is going to be a schizophrenic blog post. I’m still not quite sure who I wrote it for. The first part explains what CSRF is in some detail, and the second part goes into technicalities about a particular brand of CSRF. So if you know what CSRF and possess no delusions about the security of POST parameters, skip here.


CSRF

Cross-site request forgery (CSRF) is one of my favourite types of web application vulnerabilities. Not because of its severity – in most cases, finding CSRF in a website is only half of the problem of doing something malicious, as you also need to construct a link or an HTML form or some JavaScript mess and convince a user to click on/browse to it while logged in to the vulnerable website, through phishing or some other kind of social engineering.

Simply put, a CSRF vulnerability is a weakness that allows attackers to trick users into doing something on a website without their knowledge. It’s one of my favourite types of vulnerabilities because I think it demonstrates perfectly what a website actually is, and how you need to think about it in order to properly secure it.

In the mind of a layperson or an inexperienced web developer, an interactive website (or “web application”) is much like an ordinary desktop application, but you don’t have to install it and most of the time you get the same menu when you right-click. You’ve got your fields for entering text, your buttons and checkboxes and drop-down lists and all the rest of it. If someone wants to do something on a website, they have to visit it in their web browser, enter text in the required fields, select items from the predefined lists, check or uncheck the boxes and click on the “Submit” button. Just like a desktop application.

Except, in reality, that’s not how it works at all. All the buttons and drop-down lists are a pretty interface to text. The web server doesn’t intrinsically know or care about your HTML pages: it just processes text sent to it by users around the world.

So let’s say I have a site called namedb.com. It collects names people send to it (get your wallets ready for the IPO). On that site I have a registration and login mechanism of the usual sort, and also a form that looks like this:


Select your last name from this list:

What the server sees when someone submits it is a request, which looks something like this:

POST /add-name HTTP/1.1
Host: namedb.com
Cookie: <value to show which user is logged in>
Other: headers...

firstname=David&lastname=Not listed

Which could just as easily become, through the use of some tools as basic as telnet or as sophisticated as Burp Proxy:1

POST /add-name HTTP/1.1
Host: namedb.com
Cookie: <value to show which user is logged in>
Other: headers...

firstname='; DROP TABLE names;-- &lastname=In  my younger and more vulnerable years my father gave me some advice that I’ve been turning over in my mind ever since. “Whenever you feel like criticizing any one,” he told me, “just remember that all the people in this world haven’t had the advantages that you’ve had.” He didn’t say any more, but we’ve always been unusually communicative in a reserved way, and I understood that he meant a great deal more than that. In consequence, I’m inclined to reserve all judgments, a habit that has opened up many curious natures to me and also made me the victim of not a few veteran bores. The abnormal mind is quick to detect and attach itself to this quality when it appears in a normal person, and so it came about that in college I was unjustly accused of being a politician, because I was privy to the secret griefs of wild, unknown men. Most of the confidences were unsought — frequently I have feigned sleep, preoccupation, or a hostile levity when I realized by some unmistakable sign that an intimate revelation was quivering on the horizon; for the intimate revelations of young men, or at least the terms in which they express them, are usually plagiaristic and marred by obvious suppressions. Reserving judgments is a matter of infinite hope. I am still a little afraid of missing something if I forget that, as my father snobbishly suggested, and I snobbishly repeat, a sense of the fundamental decencies is parcelled out unequally at birth.

And if you as the maker of this form haven’t considered the possibility of this sort of input, well, then you’re going to have a lot of problems.

Thankfully, pretty much all of the modern, widely-used web frameworks have some controls in place to prevent this sort of thing by default, and contain methods and tried-and-tested best practices for implementing certain functionality that, if followed, should leave you reasonably confident that your site can only be used as you intended it. ASP.NET, for example, has a feature that checks whether a submitted drop-down box value is actually listed in that drop-down box, and most things have some way of saving you from Little Bobby Tables.

What’s so insidious about CSRF is that it’ll bypass all of this input validation, because an ordinary CSRF attack uses a totally correct request, indistinguishable from one sent by a user themself.

To continue the example with the form above, let’s say I sent you a link in your email. You click on the link, and it goes to a page with a button that looks like this:

And then you click the button. Maybe I fill the page with a fake survey, or make the button invisible and coerce you into unknowingly clicking on it with an HTML5 game, or do something else that totally convinces you to click the button. It could happen. Or I could skip the trickery and write some JavaScript code to press the button upon page load, which would be less creative but more effective. Yeah, I’d probably do that.

If you check out the source HTML for this page, you’ll see that I’ve hidden the form that that button submits2, and it’s identical in all the important respects to the form above. So, if it were a real form that did something and you clicked the button, namedb.com would receive a request from you that looked something like this:

POST /add-name HTTP/1.1
Host: namedb.com
Cookie: <value to show which user is logged in>
Other: headers...

firstname=David&lastname=Not listed

The astute reader will pick up that worst part of all this is that, because this request is going to namedb.com and comes from you, your browser helpfully adds the cookie data for your logged in session on namedb.com to the request I just tricked you into sending without your knowledge. The namedb.com server sees no difference between that and what would have been sent had you deliberately filled in the name form on the site itself, because, from a functional point of view, there is none.

This is all pretty innocuous with our namedb.com example, but imagine for a moment that instead of namedb.com, the site in question was a bank, and instead of making you send a name to a database, I was making you transfer a whole lot of money to my account. Now, as with input validation, most modern web frameworks have methods of avoiding this built into them: Ruby on Rails, I know from personal experience, more-or-less makes it a non-issue by including CSRF meta-tags on every page.


JSON

So we’ve discussed CSRF with POST parameters, and you can read about its simpler cousin GET parameter CSRF elsewhere, but there’s a third type of CSRF that’s not quite as common. It involves another way in which in which browsers send information to web servers apart from traditional GET or POST parameters, and that’s with XML/JSON POSTs or PUTs. This method is the modus operandi of popular single-page JavaScript frameworks like Angular.js and also how my own (other) site Crushly works.

Broadly speaking, a JSON/XML request is very similar to an ordinary POST request, but you need to make one small extra consideration: getting around the JavaScript same origin policy, which has some fairly strict rules about sending JSON and XML.

Consider the following code, written for a version of namedb with a REST API.3

<!DOCTYPE html>
<html>
  <body>
    <script>
      window.onload = function() {
        var xmlhttp = new XMLHttpRequest();
        xmlhttp.open("POST", "http://namedb.com/add-name");
        xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
        xmlhttp.withCredentials = true;
        xmlhttp.send(JSON.stringify({"firstname":"David", "surname":"Not listed"}));
      }
    </script>
  </body>
</html>

If I were to make a page with this in it and have you navigate to it, nothing would happen, except your browser’s JavaScript console would come up with an error message like this, if you cared to look:

Forged because namedb.com is not a real website.

This is because instead of just sending JSON and XML data to arbitrary places on the ‘net, modern browsers will actually check if the site that’s being targeted has cross-origin resource sharing enabled and whether the site sending the request is actually permitted to do so by the request’s receiver. It sends an OPTIONS HTTP message to the site to find out, and if it’s answered in the negative, no request is sent. This sort of behaviour has been dubbed “pre-flight checks”.

But such checks can be dispensed with in certain circumstances. This particular check, as it turns out, is not a feature of the function xmlhttp.send, which sends the request, but a precaution taken when it is used to send certain data types, such as application/json.4 If we change the relevant line in the code to a general data type, as below, no such precautions are taken.

xmlhttp.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");

But, of course, this will only work if the server and page/route to which the request is sent accepts its JSON in text/plain payloads. If it fussily insists on application/json and rejects all else, then I just have to hope your browser is old or non-standard enough not to do any pre-flight checks. Which isn’t impossible.

So, in summary, CSRF with a REST API can be a lot more finicky and subject to uncontrollable conditions than the ordinary kind, but that shouldn’t stop you from implementing CSRF tokens (or some other control, but preferably a proven implementation of CSRF tokens appropriate to your environment). Nothing should stop you from doing that on a site where users can change specific states based on their authorisation, because you can’t rely on all of your users to have up-to-date browsers, and you definitely can’t rely on them never to be phished.

If you’d like to correct me on something in this article or offer suggestions, caveats, and clarifications, please toss me a tweet or an email. All of the above is based on my own research, and I’m still relatively new web development and security and very eager to keep on learning.


  1. This form doesn’t actually do anything.
  2. Or would see, if it was implemented.
  3. You could insert the contents of the first field right in your web browser, without any extra tools, but let’s pretend for this exercise that my form was protected by JavaScript validation which stopped you from entering ‘ characters. That would be trivially bypassable in this manner.
  4. We’re imagining these parts were the forms actually do things, in case that wasn’t totally clear.
Next post:
Previous post: