Breaking (and Fixing) Play’s CSRF Protection

Cross-site request forgery (CSRF) is sort of the opposite of cross-origin request scripting. In a CSRF attack, the attacker coerces a victim’s browser to make a GET or POST request to another site – no PUTs or DELETEs or other actions. If your site follows best practices, GETs don’t change any information, and so a CSRF GET request is relatively benign. That still leaves POST requests vulnerable.

The Play Framework provides protection against CSRF attacks by placing a token in the user’s session, a copy of which is submitted along with every POSTed form. If the attacker grabs the page on which the form appears and stores the CSRF token he sees in the form, the victim’s browser will have a different token generated when it loads the page, and the two won’t match – Play will respond to such a POST with the message

or

and evaluation of the POST request short-circuits.

In our world-facing application, we store some information regarding our customer’s behavior in their session, including the type of product for which they’re in the process of applying. That lets us look up the correct product ID in the database, lets us easily verify that the customer owns the application for which they’re requesting the information, and in some circumstances allows us to route them directly to the relevant page rather than having to visit our landing page first.

Now, if the customer does visit the landing page, or if the same browser visits the registration page, we have to clear their session to avoid leaving the wrong kind of application information in place. In fact, if they’re visiting the registration page, it’s probably because they’re a new user on the same machine, and so we should just clear the session entirely.

That makes perfect sense, if we’re only thinking about the session information we use for business logic in the rest of the app. Unfortunately, the session also includes that CSRF token we mentioned before. The browser keeps the registration page cached, as it does with most pages, and includes the CSRF token. The server, on the other hand, wipes out the user’s session information, and so if you hit the registration page without logging out first, you’ll have a CSRF token mismatch and Play won’t let you register.

We saw this problem infrequently, and it seemed always to go away on its own. One day we noticed that it seemed to be happening to customers referred by one of our corporate partners. “Oh, that makes sense,” we thought, “something is wrong with the corporate partner token.” We totally disregarded the capitalized “CSRF” and got stuck on the registration token we were handing to our prospective customer’s employer, encouraging the guy who handles that system to fix his obviously broken code.

One day, I was looking at an unrelated issue, and needed to create a few accounts in quick succession. Immediately I saw the “missing token in request body” error, and I could then reliably reproduce it. The reason we saw it more frequently from our corporate partner was that they’d often have a spouse or domestic partner register from the same machine: it was correlated to the corporate partnership, but otherwise unrelated.

The eventual fix was easy. Instead of saying something like:

I instead did:

The redirect() forces the browser to forget its caching and make a new request to the page, which re-generates the CSRF token since the existing session had been cleared. You could do something similar with web server configuration, telling the browser not to cache anything, but this way we can handle it in code and retain the benefits of caching in all other situations.