Thursday, August 29, 2013

CSRF protection in a Play+AngularJS application

From Wikipedia:
Cross-site request forgery, also known as a one-click attack or session riding and abbreviated as CSRF (sometimes pronounced sea-surf[1]) or XSRF, is a type of malicious exploit of a website whereby unauthorized commands are transmitted from a user that the website trusts.[2] Unlike cross-site scripting (XSS), which exploits the trust a user has for a particular site, CSRF exploits the trust that a site has in a user's browser.
There are plenty of articles online explaining such attacks and measures against them. The basic idea of such protection is to generate a CSRF token per session and send that token in every following request through means other than the Cookie request header (because it's the one that browser automatically set and hence the vulnerability). Typically the token can be sent back in a custom request header or in the request query string or an hidden input value in forms. Play has a built-in filter that implements such token based CSFR protection(here is a great blog article about it). However it's based on html form(with automatic query string generation). In a single-page application that relies on Play as a pure server-side API, most requests are performed by AJAX without involving html form. In that case CSRF token has to be sent through a custom request header. Our application uses AngularJS as our front-end framework. AngularJS also provides some facility to guard against CSRF (from AngularJS' documentation):
Angular provides a mechanism to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie (by default, XSRF-TOKEN) and sets it as an HTTP header (X-XSRF-TOKEN). Since only JavaScript that runs on your domain could read the cookie, your server can be assured that the XHR came from JavaScript running on your domain. The header will not be set for cross-domain requests. To take advantage of this, your server needs to set a token in a JavaScript readable session cookie called XSRF-TOKEN on the first HTTP GET request. On subsequent XHR requests the server can verify that the cookie matches X-XSRF-TOKEN HTTP header, and therefore be sure that only JavaScript running on your domain could have sent the request.
So putting pieces together here is our implementation for CSFR protection in an AngularJS + Play application. The AngularJS side is automatic, so the work is at the play side to set token in the cookie and validate it.

When login succeeds, set the cookie that AngularJS consumers:
    Ok.withSession(("username" -> username).
       withCookies(Cookie("XSRF-TOKEN", createCSFRToken(username), httpOnly = false))
createCSFRToken is a function that hashes the username to a token so that the token is not reconstructible by attackers. httpOnly = true is there so that the cookie is readable by javascript (from the same domain of course).

For the following requests, here is the code to validate the token sent in the custom header (automatically set by AngularJS):
  def validateSessionUser(implicit request: RequestHeader): Option[String] = {
    request.session.get("username").filter { username =>
      request.headers.get("X-XSRF-TOKEN") == Some(createCSFRToken(username))
    }
  }
That's it. Feel free to ask any question in the comments.