Post navigation

Rails4, AngularJS, CSRF and Devise

I’ve been working further on my application, and run into a few challenges and issues with CSRF, so I’m elaborating a bit on my earlier post. At some point my tutorials will be updated to deal with this, but for now this is a place holder that describes what CSRF protection does, where the issues lie, and what resolutions I’ve found to the overall problem.

Firstly, it seems that there are two general developer classes with Rails – those who are developing a Rails web application and therefore use Rails to create the pages, and those who are building an API using Rails, and seem to turn off CSRF protection and use an API key to authenticate (in a sense I see an API key as a long-lived username and password, so I’m not a big fan for applications that require strong security).

I’m living in a middle space – the application front-end is all AngularJS, and it’s calling Rails asynchronously using JSON. But I’m still aiming to use Devise as my authentication engine, and I want to use CSRF to protect against malicious scripts that manipulate the API without the user knowing it. The default configurations don’t really appear to deal with this situation well.

In discussing the solution, I’ll start with a simplified discussion of what CSRF protection should and shouldn’t do, and then what pieces are needed to integrate (reasonably) cleanly.

Assume you’ve logged on to an application (let’s call it “myBank”). You don’t want to put your username and password in every time you click a link in the application, so you need some way to log on and then store the fact you’ve logged on. This is done through storing an application session_id in your browser as a cookie. Whenever your browser makes a call to myBank, it provides myBank_session_cookie as part of the request, and the myBank server knows that you’re already logged in.

The web also lets you build composite applications – you can pull pieces of content from multiple sites or servers. Consider for example an application (let’s call it “anotherApp”) that uses some fonts from Google, anotherApp’s web page will make a server request of Google to get the fonts. This also makes sense. Sometimes anotherApp wants to pull in a function from Google that requires a sign-in – perhaps anotherApp wants to put a Google+ toolbar on the top of the page. To facilitate this, when anotherApp makes a call to pull in content from Google, your browser provides your Google cookies to Google, and Google can see you’re logged in from the cookie. Importantly, anotherApp never had access to the Google cookie, it just relied on the fact you were already signed in to Google and the cookie for that session was in your browser to be provided.

So far, so good. The problem arises if an app (let’s call it dodgyApp) puts those two things together. So let’s say that dodgyApp takes a punt that some users of their site are also users of myBank. They can embed in their webpage a request to myBank, for example “transferAllMyMoney.myBank”. If you’re logged into myBank and visit dodgyApp, it’ll call myBank with “transferAllMyMoney”, and importantly your browser will send the myBank cookies with it, so it will use your logged in session to do it, resulting in all your money being transferred. Clearly a bad thing, and the reason why CSRF protection is so important.

The solution is for myBank to put a CSRF token inside the session cookie. This is just a randomly generated string, but importantly it’s different for every user, and it changes every time you log in again. This token needs to be provided as part of the http request, so now dodgyApp needs to put on their page a request that includes this token that is unique to your session. This won’t work for two reasons:

The token is buried in the cookie in your browser. dodgyApp can’t get at the cookie to pull information out and put it in the http request – remember they’re relying on your browser blindly sending the cookie, they never had access to it themselves. They could arguably get at it by sniffing the network (if you’re not on an SSL connection), or other more esoteric attacks, but that’s a pretty high investment of effort

They’ve gone from a random broadcast attack in which anyone that visits dodgyApp gets a “transferAllMyMoney” request, to a specific targeted attack which must be aimed at an individual in a specific timeframe with a high level of investment to craft the url. It’s no longer a script kiddie attack.

Note that in this solution there’s no requirement to try to keep the CSRF token secret from the end user – we’re protecting against cross site requests, not against session hijacking. This is important in later discussion. Also, CSRF protection only protects POST/PUT/DELETE actions, not GET actions. This influences what methods you choose to use in your application – anything that might require CSRF protection should be implemented as POST/PUT/DELETE, not as GET.

The Rails implementation of this is pretty much as you might expect. When you send a get request down to the server it returns a session in a cookie, and embedded into the page it provides the CSRF token, which will come back in the POST request headers. Anyone attempting to create an arbitrary script that issues POST requests won’t have access to the CSRF token, and therefore cannot craft that request.

When we introduce Devise into the mix, it gets a little more complicated. Devise replaces the session at specific points in the lifecycle – namely when you log in and when you log out. If you’re using a Rails front end this is handled cleanly, but work may be required with other front ends.

When we introduce AJAX calls and parallel processing we can get further issues with the use of the cookie to store the session – in short it’s possible for your AngularJS application to send two or more requests in parallel, for those requests to get serviced by different servers or threads, and therefore for those requests to return different CSRF and session information. It’s then luck as to whether the “right” combination end up stored in your browser cookies. This problem isn’t new, in fact Paul Butcher from 2007 provides a great description of a similar problem.

There is also an interaction with the chosen session store. Your session store can broadly be cookie-based (usually cookie_store), or database based (active_record_store). These are configured in config/initializers/session_store.rb. When you use cookie_store all the contents of your session are bundled up into an encrypted cookie and shipped down to the browser. When the cookie comes back with the next request the server unpacks it, uses or updates the content, then sends it back in the response. This is quite nifty, as it lets you use a cluster of servers without them needing to share the sessions on the back-end. However, it can give parallelism problems, and it means that killing a session isn’t quite what you might have thought – since there’s no persistent storage for the session on the server side, a malicious user can bring a session back to life by simply copying the prior cookie back again. (For discussions of why this means you shouldn’t store some types of data in the session, review the session replay attack information in the rails security guide)

The database session store holds the actual session information in your database, with only the session id stored in a cookie in your browser. Again the session is shared across your servers, and it has the advantage that when you kill a session, it stays killed even if a malicious user replays the cookie. However, it can still lead to synchronisation problems with parallel requests, as Paul Butcher describes in the link above.

So, I’ll summarise the problems that I’ve identified in the setup, and then talk about the solutions that I’ve implemented to them:

When working with Rails and AngularJS, you need a way for the CSRF token to be provided by Angular in all requests, we do this by setting a CSRF cookie

When you log out from Devise, you need to provide a new CSRF cookie, as the old one will now be invalid. If you don’t do this, Angular will keep providing the old CSRF token, and all requests will fail with an invalid token (including new requests to logon)

Belt and braces, when something goes wrong and you get an invalid authenticity token error, you need to clean up the browser cookies, otherwise the user will continue getting invalid token with no way to fix it other than clearing cookies

There are a number of potential race conditions with CSRF tokens and session cookies, I have no permanent fix for these but have mitigated where possible

There is an issue in Devise with the timeout module and cookie_store that may expose your sessions to coming back to life. I’m using the active_record_store to avoid this.

I’ll work through each in order.

1. Getting CSRF token to Angular

The best answer I’ve seen for this is on stackoverflow, with the answer from HungYuHei. In short, you extend your application_controller to do two things. Firstly, set a cookie that stores the CSRF token, which AngularJS will automatically recognise. Secondly, AngularJS will insert that token into requests in a header field ‘X-XSRF-TOKEN’, you need to pull that token back out of the request header, and put it into form_authenticity_token. The code to do these two things is:

2. Providing a new CSRF token on devise logout.

When a sign_out is issued by Devise, it doesn’t seem to get picked up by the after filter on the application_controller. Devise will destroy the session (making the CSRF token in your angular app invalid), but it doesn’t issue a new CSRF token. The solution for this was also found on stackoverflow, with the important bits provided by Jimbo and Sija.

We extend the Devise sessions controller to always return a new CSRF cookie after a signout action.

We also update the routes to reflect that we have a local session controller, rather than the default one.

devise_for :users, :controllers => {sessions: 'sessions'}

3. Clean up cookies and session on InvalidAuthenticityRequest

This one is a tradeoff, depending on which session store you’re using. If someone’s CSRF token and session don’t match, then it makes sense that we should seek to fix their browser, otherwise their calls will continue to fail and they have little chance of sorting it out without clearing all cookies.

There are two possible ways to fix this:

Just give them a new CSRF cookie that is correct and matches their session. This will allow their session to keep working, and is low impact on the user. Since this is a cookie that we send back, we’re not making the information available to a cross-site request, only to that user’s browser.

Tear down the user’s session and send them a new one with a new CSRF. They’ll then have to log in again. This would mean that if someone is attempting a hack on your site then unless they get it exactly right the first time, we’ll destroy the session and they’ll need to go and find a new cookie to attempt with.

To some extent your decision may depend on which session store you’re using, with some consideration of a tradeoff between usability and security. If you’re using the cookie_store, then there’s not much point in choosing option 2, as the attacker can simply copy the previous cookie and run a replay attack. If you’re using the active_record_store, then option 2 may make more sense. Conversely, option 2 opens up the ability for an attack that forces logouts on your application. Our dodgyApp could try to make cross-site requests against our server, and those requests would result in the user being logged out of myBank. That’s annoying but not the end of the world, and may provide some indication to the user that something is happening with dodgyApp, and cause them to stop visiting it.

I’m using active_record_store, so I’m leaning towards option 2, but still considering the impact given the points I make in the race conditions section below. The code for each would go into your application controller, and reflects the change I’ve suggested on the rails security guide. With later versions of rails an invalid authenticity token will result in an exception, so you need to catch that exception and take an appropriate action. If you just want to fix the cookie, then:

4. Race conditions

You need to consider your application and the potential for parallel requests. My application will often issue 2 or 3 requests against the API in parallel. Each of those requests could individually update the CSRF token and session. Again, Justin Butcher’s discussion of the potential here is better than any I could write.

Consider the following scenario:

Your application fires off requests for item_a, item_b and item_c

Each request goes to a different application server, so they are processed in parallel, which request responds first depends on the load on each server (or thread) at that point in time

If each request is capable of returning a new CSRF token, a new session or some combination of the two, then the ending state on your browser is quite unclear. It’s easy to get intermittent authenticity token errors without expecting it.

The behaviour will also depend on which store you’re using. If you’re using cookie_store and not storing anything critical in your session, then really so long as you get a CSRF token that matches the session, things are good. Any of the responses will do.

If you’re using the active_record_store, then if each request updates the CSRF token, only the last server update will be stored in the database session at the end. If the servers process the requests in the order item_a, then item_b, then item_c, then the database will have stored the CSRF token that was returned with with item_c. If your browser processes the responses in the order item_a, item_c, item_b, then the browser will store the CSRF token from item_b, which is invalid.

My current response to this is to mitigate not prevent – since I haven’t worked out a way to prevent as yet that doesn’t involve removing all the asynch processing. The key elements are:

I’m using active_record_sessions. This means the only thing going into the cookie in the browser is the session id, therefore the session will be very infrequently updated

I’m seeking to ensure that any transaction that returns a new session also returns a CSRF token. I previously had some calls (particularly devise sign_out) in which a new session was returned without a new CSRF token, and this immediately made all future requests invalid.

I’m minimising the number of asynch requests that happen around sign_in and sign_out, as these are the places where the CSRF token definitely changes. By avoiding my application making parallel requests at this point, I reduce the potential for the cookies to get out of synch. This means that I call “sign_in”, wait for the response, then I fire off a bunch of calls to get what I need. This sort of makes sense anyway, as those calls probably rely on you being signed in to return their data.

This area is not yet perfect, but I think it’s sufficient for the purposes of my app. If I get problems in production with authenticity errors, I may look to revisit my decisions in section 3 above, as my decision there would mean that each intermittent error we get will log out the user.

5. Devise and Timeout

The Devise functionality includes a timeoutable module. This module notices when a session has had no activity for more than a configurable period (say 20.minutes), and terminates the session. This functionality provides “logged out for inactivity” functionality, which is useful for annoying your users :-), but also an essential security provision.

The threat vector is that an attacker has access to a session cookie. This could be through someone not logging out on an internet cafe computer, from browser history logs, or any of a number of other mechanisms. Without the timeout logic a cookie that an attacker finds from days or weeks ago would remain valid.

In my application I’ve identified an issue where some code that I’d written resulted in the timeout logic being bypassed – effectively sessions remained valid despite having previously been timed out. The details for this are on the Devise issues tracker, in summary if you access the user session before the timeout logic has a chance to run, it appears to result in the timeout logic not running at all. I’d anticipate the issue being resolved in the not too distant future. Having said this, the issue appears to occur when there is poor coding on my part, and I haven’t yet identified where in my application this poor coding is. I’m choosing to move to the active_record_store, as I think that guarantees that future poor coding on my part could not reintroduce the issue.

Maybe a solution to mitigate the race condition problem is two have two valid tokens with a lifespan (for example: 1 minute) and swap them when expire. If some request reaches the server, the csrf token will be validated against them, and regenerate the oldest one.

Session: [CSRF_token1 ttl: 60s, CSRF_token2 ttl: 120s]

Then the race condition is really improvable, and an attacker have to execute attack in less than a minute.

Hello, I wanted to thank you for all your tutorials. I’m getting stuck on this one and would love to know if we could do a paid session over skype to troubleshoot it. Please let me know if you’d be interested. Thank you!

Hi Paul ! Thanks a lot for this post. I found it as I’m stuck with authentification. I’m digging into a login successful and a cookie that is immediatly destroyed after the logged in action. Your post is part of the response. I continue investigating !

Hey, great article. I have a very important edit for users of rails 4.2+. Rails introduced csrf token masking, which basically encodes the csrf token with a one time pad for each request. This causes the “form_authenticity_token” to be different on each request, even though the underlying csrf token has not changed.

Oh, Hallelujah! Finally somebody walked me through why a (seemingly) working login chain would only work once, and how to fix it. I’m not even using Angular, but just seeing how Devise logout clobbers the CSRF cookie and what to do about it is a huge missing piece from every other AJAX/Devise integration I’ve found.