9
votes

Our error logs occasionally contain legitimate form submissions that cause ActionController::InvalidAuthenticityToken errors.

My hypothesis is that the CSRF token stored in the user's session cookie has changed at some point after the form was loaded but before it was submitted. This causes a mismatch between the POSTed token and the token in the cookie, leading to this error.

Given that a Rails session cookie expires only when the browsing session ends (ie when the web browser is closed), what are the ways in which this cookie (and the CSRF token it includes) can be changed without closing the browser?

We are using cookies to store session data, which is Rails' default behaviour.

5

5 Answers

8
votes

The user could have logged out and back in, but had a tab with a form open from the old session. That would send the old token.

5
votes

Here's what we know:

  • Legitimate form submissions that cause an InvalidAuthenticityToken exception have somehow lost their original CSRF token.
  • The token is kept in the session, which is kept in an encrypted cookie. So this error means their session cookie has changed since the form was generated.
  • The session cookie does not expire unless the user's browser window is closed.

I now believe there are two ways that invalid CSRF tokens can be submitted by legitimate users.

Note that these apply specifically to Rails 4.2. As I understand it, the "per-form CSRF tokens" feature in Rails 5 may mitigate them.

1. Session change in another tab

As per @crazymykl's answer, the user could open the form, then log out in another tab. This would cause the session cookie stored in the user's browser to change. When they came back to the original tab and submitted the form, the token from the form would not match the token in the session, and the error would pop.

2. Caching

As per this rails bug, Safari behaves oddly with caching under some circumstances. Telling it to reopen with the same windows as last time (via Safari > Preferences > General), opening the form and quitting Safari results in the form being redisplayed.

Submitting that cached form causes a CSRF error. As the opener of the bug concludes, it appears that Safari caches the page, but drops the session cookie. Hence the mismatch.

The solution to this problem is to set a Cache-Control header with the directive no-store. (Difference between no-cache and no-store explained here).

Further examples welcome :).

1
votes

Are you using Ajax form to submit the request? Does your page has multiple forms? If so, check your code whether correct csrf token is submitted along with the request. We had similar issue that page is rendered with one csrf token and we used this token to submit form one and we would have got another csrf token but the second was sending old csrf token that result in error. I am not sure how this is helpful but want to share similar problem faced by me

1
votes

You wrote: Given that a Rails session cookie expires only when the browsing session ends (ie when the web browser is closed), what are the ways in which this cookie (and the CSRF token it includes) can be changed without closing the browser?


First off, your hypothesis is valid. How you got there might be worth considering, though.

The presumption you present needs focus on two levels.

One: a cookie stored does not get deleted when the web browsing session ends unless something has been coded that way; the cookie is likely persistent until he cookie timeout, so it's likely that the next access of the page will use the old token, but because developers generally allow the "new login" to refresh the page, they are likely to also refresh the token at that time. See @Shikhar-Mann response to better understand the sign_out_user.

Two: the cookie doesn't have to be changed for this problem, it's the mismatch of the CSRF token that is the issue.

So the root question should be: what are the ways that we can have a mismatched CSRF token, which would be easier to answer: old data on client due to a long wait, which causes a timeout on the server, which invalidates the CSRF token during the delay. If the web page is not configured/created to also time out and redirect, the client / user would never know.

Also, might I suggest that the CSRF NOT be persisted? It's really not valuable to do so if you can access form data; typically I create a hidden field with the CSRF data and use that to post back instead. CSRF doesn't live very long, and session data is made to persist.

0
votes

If closing browser is not an option. Then you got to log out the user. To achieve that place the following code in ApplicationController.

rescue_from ActionController::InvalidAuthenticityToken do |exception|

sign_out_user # Example method that will destroy the user cookies

end

P.S: Above is from http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf so refer that for more information.