Jan 6, 2012

Stateless CSRF Protection

In the era of RESTful services and rich internet applications it's important to find security solutions that don't impose unnecessary state or computation on servers. I previously wrote a post on stateless session ids. Let's have a look at how we can protect against cross-site request forgeries (CSRF) without server-side state.

CSRF Basics
Forged requests are nasty attacks. They rely on the fact that your browser automatically adds cookies to HTTP requests if it has cookies associated with the target domain and path. That includes session cookies.

Let's say you're currently authenticated to twitter.com. If you visit another site on another domain that site can issue requests to twitter.com and your Twitter session cookie will be added to those requests.



How can domain B issue requests to domain A, formally doing a cross-site HTTP request? Well, there are some obvious cases – images, JavaScript, and CSS.

<img src="whatever.domain.org/path/logo.png" />

… is allowed from any site, which means a malicious site can contain tags like …

<img src=”https://secure.bank.com/checkAccounts" height=0 width=0 />

Such a tag will issue an HTTP GET to secure.bank.com/checkAccounts including the victim's session cookie for *.bank.com should he or she be logged in. The browser doesn't know if there's an image on that URL or not. It just fires the request. And by setting the image size to 0x0 the victim will see nothing.

CSRF With POST
Most sensitive stuff require an HTTP POST since a GET should be idempotent and not change any state server-side. So can a malicious page issue an HTTP POST to any domain? Yes.



The CSRF code from the image above ($ is jQuery):

<form id="target" method="POST" 
 action="https://1-liner.org/form">
  <input type="text" value="I hate OWASP!" name="oneLiner"/>
<input type="submit"
 value="POST"/>
</form>


<script>
  $(document).ready(function() {
     $('#target').submit();
  });
</script>

CSRF Against RESTful Services
But maybe you've left HTML forms behind and go with rich clients, a RESTful backend and communication via JSON? Can a malicious page issue an HTTP POST targeting such services? Yes.

You can change the encoding of HTML forms to text/plain and do some tricks to produce parseable JSON in the request body. Here's an example that I got working with a Java JAXRS backend:

<form id="target" method="POST" 

action="https://vulnerable.1-liner.org:
8444/ws/oneliners" 

style="visibility:hidden"
 enctype="text/plain">
  <input type="text"

   name='{"id": 0, "nickName": "John",
          "oneLiner": "I hate OWASP!",

          "timestamp": "20111006"}//'
   value="dummy" />
  <input type="submit" value="Go" />

</form>

Notice the enctype and that the JSON is in the input name, not the value. The above form produces a request body looking like this:

{"id": 0, "nickName": "John","oneLiner": "I hate OWASP!","timestamp": "20111006"}//=dummy

… which is accepted by for instance the Jackson parser.

CSRF Protection With Double Submit
Traditional anti-CSRF techniques use tokens issued by the server that the client has to post back. The server validates the request by comparing the incoming token with it's copy. But that small word "copy" means server-side state. Not good.

Double submit is a variation of the token scheme where the client is required to submit the token both as a request parameter and as a cookie.



A malicious page on another domain cannot read the anti-CSRF cookie before its request and thus cannot include it as a request parameter.



Two Misconceptions About Double Submit
There are two common misconceptions about the double submit CSRF protection.

First, it has been suggested that the session cookie should be used for this purpose. Since you have to use JavaScript to pick up the cookie value and add it as a request parameter the cookie cannot have the HTTPOnly attribute. And you want HTTPOnly on your session cookie to prevent session hijacking via cross-site scripting.

But you should not use the session cookie as anti-CSRF cookie. Instead add a specific anti-CSRF cookie which does not have the HTTPOnly attribute and keep your session cookie protected.

Second, people have stuck with server-generated, stateful anti-CSRF cookies. But double submit cookies can be generated client-side and don't have to be saved by the server at all.

Stateless CSRF Protection with Double Submit
The protective measure of double submit lies in the fact that a malicious site cannot read the cookie and include it as request parameter. That condition still holds if the cookie is generated by the client and never saved by the server.

So let the client generate the anti-CSRF value and only compare and check format of cookie and request parameter on the server. Ergo, stateless CSRF protection!



Hardening the Double Submit Protection
Double submit protection breaks down if the attacker somehow can read or set the anti-CSRF value. We can harden double submit against malicious reads.

First of all we make the client change the anti-CSRF value upon every request. This is typically done by centralizing backend calls to a custom AJAX proxy, possibly inherited.

Second, we zero the anti-CSRF cookie directly after each backend call. This will allow for accurate server-side detection of forged requests. A zeroed double submit cookie is a clear signal of either a client-side bug or a forged request. With zeroed anti-CRSF cookies the attacker has to issue his/her attack to exactly when the cookie is set by the client.

Drawbacks of Double Submit
You typically hear two drawbacks of the double submit protection – it's reliance on JavaScript to add the cookie value as request parameter, and the possibility to read the anti-CSRF cookie via cross-site scripting.

The issue with JavaScript is diminishing as JavaScript is becoming a requirement for more and more sites anyway.

The cross-site scripting critique is invalid. If you can script the site you already own all of it and can setup your own AJAX proxy, read any tokens in the DOM etc.

16 comments:

  1. If I read this right, the approach you're suggesting doesn't work if have xss in any subdomain, since false csrf-cookies can be injected to override the legit csrfcookies. For example, xss on the non-critical developer.mozilla.org could be used for CSRF against addons.mozilla.org: https://bugzilla.mozilla.org/show_bug.cgi?id=648881

    I think there's a safer method for stateless csrf defence involving hashing the user's userid and password with a timestamp.

    ReplyDelete
  2. Er sorry about the the dead link; it's long been patched and should be made public shortly.

    ReplyDelete
  3. Travis Rhodes (@travisrhodes)January 6, 2012 at 10:52 PM

    This approach can be perilous. Unless those duplicate tokens are tied to the authenticated user or otherwise verified, this can be bypassed by a related domain (subdomain of the same root domain). IOW, an XSS in my.blogspot.com can be used to SET cookies in your.blogspot.com. You menioned that one needs to "check the format" of the cookie and token, but more explanation is probably needed. Check out our 28c3 preso here: http://www.youtube.com/watch?v=hB2lPJldYQI (at 18:30) and whitepaper here: http://ab.m6.net/bh/BH2011_whitepaper.docx

    ReplyDelete
  4. Thanks albino and Travis. You both bring up a valid point – the attacker may be able to set the cookie via an XSS on a subdomain of the target site. Oldest reference I have of the problem is from 2008: http://kuza55.blogspot.com/2008/02/understanding-cookie-security.html

    I will either update this blog post or write a new one on the subject. Needs to be clarified for sure.

    ReplyDelete
  5. Hi,

    I think the CSRF cookie should be HTTPOnly. There is no need to access that cookie with JS.

    When the browser asksfor a page that contains a form, the server generates a token and it is inserted as input in the form, and delivered as a HTTPOnly cookie to the browser. When the browser sends the form, the browser attach the cookie,and the server compares both values.

    An attackant won't be able of tamper thecookie or guess the form value.

    ReplyDelete
  6. vtortola - there's no form. There's just RESTful services. There could of course be a service which you would have to call before calling every other service to get the CSRF cookie, but I don't think that's REST anymore.

    But John - I didn't quite get "change value on every request", is the server suppose to be able to spot this reuse? That would mean to save some state? Your 2nd hardening technique, I assume the server side should always set the cookie value to 0, but never expect the cookie value 0.

    Thanks!

    ReplyDelete
  7. Very late reply ...

    vtortola: The cookie cannot be HTTPOnly since we're talking Ajax calls to RESTful services and not form-based POSTs.

    Joffemannen: The reason to change value every request is to prohibit replay. If an attacker somehow captures a double submitted value that value should not be valid again. By also clearing the cookie in between Ajax calls the client is most often in a non-request state. That means CSRF attempts can even be detected server-side.

    ReplyDelete
    Replies
    1. If the cookie cannot be HTTPOnly then this is not secure solution. The Double Submit Cookies was designed for form-based POSTs, not for RESTful services, where you can set HTTPOnly on CSRF cookie.

      Delete
  8. John,
    I completely agree regarding the double submit solution. I think it is a pretty solid scheme, but almost all discussions about this online dismisses this solution on the above on the basis of the misconceptions mentioned.

    Another idea (misconception?) I've come across is that the cookie needs to be cryptographically random. I'm thinking it doesn't, the simplest form would be a binary 1|0 (assuming the cookie is reset by the server each time).

    Another replay-protection I thought of which does not require server side state would be to use a checksum of the posted form as token value. That way if the cookie would reside in the browser for whatever reason, it can only be used to post a certain form, again => difficult to turn into something fun/evil.

    That does not solve the subdomain dilemma though. Perhaps a third cookie could be used, set by the server as HASH(client_ip+salt_of_the_day), scoped to the proper subdomain (addons.mozilla.org). That one could not be read by an xss:ed subdomain (developer.mozilla.org), thus not included in the form post. The only server side state involved would be a global salt of the day - and only an actor with large ip-ranges would be able to even try bruteforcing it.

    ReplyDelete
    Replies
    1. Martin & John;

      It needs at a bare minimum be sufficiently random and sufficiently securely removed.
      Consider a failure to clear cookie, you could bruteforce the value of the cookie by numerous concurrent csrf. Or in martins example, race condition would be trivial. Also, prng might be cracked if js Math.random() is used as source, making browser specific csrf attacks possible.

      Delete
  9. An attacker can insert the CSRF token as cookie and also send it in request. He can bypass the CSRF protection once he detects that this is the scheme used.

    ReplyDelete
  10. @Anon: How do you propose the attacker inserts a cookie?

    If the attacker has XSS on the same page or app the whole point of anti-csrf is lost anyway.

    If the attacker has XSS on the domain it is clearly possible but has to be done in two stages.

    If the attacker has XSS on another subdomain it is possible only if the anti-csrf cookie is valid for that subdomain (e.g. csrf-protected site is ssl.example.com, XSS on open.example.org, and anti-csrf cookie has host .example.com). That's a flaw in the CSRF protection though.

    I covered these issues and more during a talk at OWASP BeNeLux about a month ago. I will publish the slides on SlideShare after my talk(s) at GeekMeet in two weeks.

    ReplyDelete
    Replies
    1. I meant *.example.com in all cases, not example.org.

      Delete
  11. If you have REST APIs that use send and receive JSON, you can restrict the APIs to only accept Content-Type: application/json. The attack using the form is not possible with this restriction, because HTML forms can only submit 3 Content-Types: application/x-www-form-urlencoded, multipart/form-data, and text/plain. The example you showed submitted text/plain data, but it was parsed by the server as JSON.

    ReplyDelete
  12. Thanks a lot for this elegant and simple solution, which seems very suitable for our use case of rich JS-based UIs interacting with Rest services.

    ReplyDelete
  13. Great post!

    It is very informative and helpful code of csrf token

    ReplyDelete