Rails: CSRF protection prevents some webkit users from submitting forms

Created on 13 Oct 2015  ·  102Comments  ·  Source: rails/rails

Hi,

We've recently been investigating reports from our users that they are unable to submit forms.

Upon investigation it appears that browsers can get in a state where Rail's CSRF (Cross-Site Request Forgery) protection stops the form being submitted.

To reproduce

It's possible to produce a minimal Rails app which has this problem:

rails new csrf-test
cd csrf-test
bundle exec rails generate scaffold Test test:string
bundle exec rake db:migrate
bundle exec rails server

How to replicate it on mobile Safari (tested on iOS9):

  • Load a page containing a form (will be http://localhost:3000/tests/new in this example).
  • Quit Safari by double-tap the home button and swipe up.
  • Open Safari from the home screen. You should see the same page with the form.
  • Submit the form.

You will see the Rails invalid authenticity token error- this is a "The change you wanted was rejected" message in production, or an ActionController::InvalidAuthenticityToken in development. I've also made a video that follows theese steps.

How to replicate on Desktop Safari (tested on Safari 9.0 on OSX)

  • Go to 'Safari' > 'Preferences...' > 'General' and set 'Safari opens with:' to 'All windows from last session'.
  • Load a page containing a form (will be http://localhost:3000/tests/new in this example).
  • Quit Safari (with CMD+Q)
  • Open Safari. You should see the same page with the form.
  • Submit the form.

This problem seems to happen regardless if:

  • the app is served HTTP or HTTPS
  • the app's environment is development or production
  • the browser is manually quit by the user, or quit by the OS (to save memory)

I have also been able to replicate on Chrome on Android. I haven't yet been able to replicate it on Chrome and Firefox on OSX using their 'restore tabs' options like I did in Safari. There may be other browsers that are affected.

What's happening

Looking at the Rails logs, and the cookie submitted by the browser I believe that the browsers are caching the page, but clearing session cookies. This means the form has a authenticity_token parameter, but the Rails session cookie has been cleared so has no corresponding _csrf_token.

Here is a annotated log showing this:

# Browser loads the form for the first time
Started GET "/tests/new" for 127.0.0.1 at 2015-10-13 09:23:18 +0100
  ActiveRecord::SchemaMigration Load (0.1ms)  SELECT "schema_migrations".* FROM "schema_migrations"
Processing by TestsController#new as HTML
  Rendered tests/_form.html.erb (37.0ms)
  Rendered tests/new.html.erb within layouts/application (41.4ms)
Completed 200 OK in 256ms (Views: 243.3ms | ActiveRecord: 0.3ms)

# (Asset requests ommited)

# Browser quits, clearing session cookies
# Browser re-opens, reloads the page from cache without doing a request

# Browser posts the form:
Started POST "/tests" for 127.0.0.1 at 2015-10-13 09:23:37 +0100
Processing by TestsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"IhsNUyL6Y/riLIujH+ExkTZN9pEPfwAVVB/t9pwrnkIR6lw1bAl3ZFY+bPg+zqMf3pj3qeY0vgbKblrWgr0vnQ==", "test"=>{"test"=>""}, "commit"=>"Create Test"}
Can't verify CSRF token authenticity
Completed 422 Unprocessable Entity in 1ms (ActiveRecord: 0.0ms)

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
  actionpack (4.2.4) lib/action_controller/metal/request_forgery_protection.rb:181:in `handle_unverified_request'
  actionpack (4.2.4) lib/action_controller/metal/request_forgery_protection.rb:209:in `handle_unverified_request'
  actionpack (4.2.4) lib/action_controller/metal/request_forgery_protection.rb:204:in `verify_authenticity_token'
  # (Stack trace truncated)

I've tried to find more documentation about this kind of caching that browsers do. I found this article about the WebKit Page Cache but it appears to be out of date (it says HTTPS pages do not use the Page Cache, but I have seen this problem on HTTP and HTTPS pages). If anyone can find more about this I'd love to know.

I'd like to know:

  • Have others seen this problem? I haven't been able to find reported before, or find any documentation regarding this, but perhaps I have missed something.
  • Is there a way that Rails could be changed that would prevent this happening?
  • Are there any workarounds for this that we could apply in our application?

Thanks for any help.

needs feedback stale

Most helpful comment

Hi, I've been following this issue for quite some time now, and I'm just wondering.

To developers suffering from this issue, does your project use Devise for authentication?

Please react to this comment with :+1: for yes, :-1: for no.

All 102 comments

Is there a way that Rails could be changed that would prevent this happening?

I can't see any way to prevent this happening without opening the application to possible CSRF attacks.

@zetter does changing the Cache-Control header to no-store, no-cache make any difference?

@zetter does changing the Cache-Control header to no-store, no-cache make any difference?

I've added this line in the application config:

config.action_dispatch.default_headers.merge!('Cache-Control' => 'no-store, no-cache')

Mobile Safari has the same problem, however I can no longer reproduce this in Desktop Safari and Chrome on Android.

it does feel odd to me that the page is no-store but this isn't respected by Mobile Safari but is by the other browsers. Perhaps this is a bug in Mobile Safari although, as I said above I found it very difficult to find documentation around the expected behaviour of this kind of page caching. Perhaps there is another combination of cache headers that makes this or another work around for Mobile Safari?

UPDATE: I found that Mobile Safari does respect the 'no-store' header, however adding the header and refreshing the page isn't enough. For it to take effect also had to clear the cache using the 'Clear History and Website Data' option from Safari Settings.

This problem might also be avoided if the CSRF token was in a persistent cookie, but I'm not sure if there are security implications for doing so. It would be really helpful if anyone could explain to me if this would be fine, or a dangerous thing, to do.

interesting topic, i see this error quite often.
@zetter here there's some mention about CSRF token in cookies, but it's used with javascript for caching purposes https://www.fastly.com/blog/Caching-the-Uncacheable-CSRF-security

An update:

  • I've deployed the change to serve all pages of our application to use a 'no-store' header. We're tracking how many times we see invalid authenticity token errors so next week I should be able to report how effective this change was for us.
  • I've tried working round the issue in Mobile Safari by experimenting with headers and client side JS (such as hooking into load/unload events) but have been unable to. I plan to raise a bug with Webkit/Apple, and will link to this when I do.
  • I'm curious how other frameworks handle this problem, for example, I know that Django uses a similar mechanism for CSRF prevention so will check to see if they have already solved this problem.

@alepore Thanks for sharing. I'm not sure if any of the solutions there help since it's they are about solving the problem of being able to use CSRF prevention token in combination with server-side caching.

My investigation into Django:

Django uses a similar mechanism to rails to prevent CSRF attacks- a token is stored in a cookie is compared to a tokens submitted with a form. What is different is how they store the token in a cookie in the default case:

Comparing Rails and Django

Rails adds the token to the session cookie under the _csrf_token key. Since the Rails session cookie has no expiry set, it will be cleared when the browser closes.

Django puts adds the token in it's own cookie called CSRF_COOKIE. This is a persistent cookie that expires in a year. If subsequent requests are made, the cookie's expiry is updated.

Why does Rails behave in this way?

Rail's CSRF protection comes from the csrf_killer plugin. The plugin had the following warning:

Make sure the session cookies that Rails creates are non-persistent. Check in Firefox and look for "Expires: at end of session"

This warning got imported into Rails from the csrf_killer documentation in https://github.com/rails/rails/commit/2c73115b2fd1c547a8cf543a41b8b8b9d04925e1. It has since been replaced in https://github.com/rails/rails/commit/7d8474e20a7c3ee720c2659e52d1862dcd8b368d by this warning

It is common to use persistent cookies to store user information, with cookies.permanent for example. In this case, the cookies will not be cleared and the out of the box CSRF protection will not be effective.

Read full section of the guide. I'm not sure if "the cookies will not be cleared" in the statement above is referring to when a user signs out, when a session ends, or when an invalid authenticity token is given.

Why does Django behave in this way?

In contrast, it looks like Django made the explicit decision to store CSRF prevention token in a permanent cookie. From the Django docs:

The reason for setting a long-lived expiration time is to avoid problems in the case of a user closing a browser or bookmarking a page and then loading that page from a browser cache. Without persistent cookies, the form submission would fail in this case.

You can also read the bug which caused this change in Django, a question on the Django mailing list asking if persistent cookies are safe and a related bug for how Internet Explorer can be configured to block persistent cookies.

Is it secure to store the CSRF prevention token in a separate permanent cookie?

Storing the token in a permanent cookie would fix the original issue as it would no longer expire when the browser closed. It looks like Django has made the choice that it is. I couldn't find any information around CSRF attacks that suggests it wouldn't be. The only item I found was on the OWASP Wiki which suggests that the shorter the lifespan of the token the better, without any justification:

To further enhance the security of this proposed design, consider randomizing the CSRF token parameter name and or value for each request. Implementing this approach results in the generation of per-request tokens as opposed to per-session tokens. Note, however, that this may result in usability concerns. For example, the "Back" button browser capability is often hindered as the previous page may contain a token that is no longer valid. Interaction with this previous page will result in a CSRF false positive security event at the server.

I'd love to know from anyone who knows more about CSRF attacks to find out if it would be safe to store the CSRF prevention token in a permanent cookie (both for our application, and for Rails).

Thank you for the investigation and writeup, @zetter. Agree with Django's reasoning and decision 👍 May be a bit tricky to introduce to Rails apps in a compatible way, though.

I can confirm the issue, combination El Capitan + Safari + http2, not a single working form... However there is no problem with Safari on Yosemite...

Edit: after disabling HTTP/2, the problem disappears

Another update:

Here's a graph of showing the number of our users seeing form authenticity token errors as a proportion of the number of visits to our site. It looks like the rate of errors have dropped since we introduced no-store cache header.

screen shot 2015-11-09 at 09 00 10

Note that the traffic pattern to our site is unusual as a lot of our users visit weekly, this means they may have a page open in a browser from a visit a previous week. I think that's why even though there was an initial drop in errors, another drop came a week later as the cohort of the previous weeks refreshed the pages and got the updated headers.

I still plan to:

  • raise a bug with Safari/Webkit about the inconsistent behaviour of no-store as compared to other browsers (UPDATE: I found Safari is respecting the no-store header, it just needs to be reset- see above comment for more)
  • try storing the token as a permanent cookie in our app to see if error rates are further decreased

@zetter Just wondering, is your server HTTP/2 enabled?

@zetter Just wondering, is your server HTTP/2 enabled?

@aganov It is not, and I have not tested my original example with a HTTP/2 server.

My understanding of HTTP/2 is that there are no changes to cookies or headers so the behaviour I am seeing shouldn't change. You said before that there wasn't a single working form- were you following my original reproduction example? or is this another app that might have a different problem?

try storing the token as a permanent cookie in our app to see if error rates are further decreased

You can try that by giving your session cookie an expire_after value to persist it across browsing sessions:

Rails.application.config.session_store :cookie_store, key: '_myapp_session', expire_after: 2.weeks

You'll need to consider possible security implications based on your session data. In my case, it solved the problem.

+1 for moving the CSRF token to its own permanent cookie.

I saw a case a few years ago where the Django CSRF behavior resulted in a security vuln for an app. With an XSS on foo.example.com, an attacker was able to set the CSRF token cookie for .example.com causing it to be sent with requests to example.com and *.example.com. The attacker-controlled CSRF token clobbered the one that had been set by the site at example.com. Because the attacker knows the new CSRF token value, they can then perform CSRF attacks.

This sort of attack doesn't affect Rails applications because the CSRF token is stored in a signed cookie. Most applications will reset_session during login, so even if an attacker can set an unauthenticated cookiestore cookie (session fixation), the user will receive a new CSRF token after authenticating.

Django's CSRF token storage seems like a pretty big weakness to me. If you decide to store Rails CSRF tokens in a separate cookie, they should definitely still be signed.

As for the security impact of using a permanent cookie, the risk seems the same as using a permanent cookie for the whole cookiestore session. The CSRF token is generally the most sensitive thing stored in a session, so if there's anything that should be deleted when the browser is closed it's that. This issue seems like a Safari bug to me. Safari is notoriously bad at handling cookies. I think suggesting that apps work around the issue by using a permanent cookie for the session is better than working around the Safari bug in the framework.

Try changing "protect_from_forgery with: :exception" to=> "protect_from_forgery with: :null_session"

:null_session - Provides an empty session during request but doesn't reset it completely. Used as default if :with option is not specified.

For me, using :null_session, together with some straight devise usage, it ended up in an infinite redirection loop in an Safari/iPad. For the time being I am using Chrome for iPad.

Any thoughts or update on this? I think its hidden a lot in the default mode, as people just get a null session and we quietly log it. I've switched to raise and now I can see this is affecting a fair few people.

I've been seeing something similar since switching to the :exception behaviour. Specifically from unauthenticated endpoints. We are also mostly an Angular/JSON app so CSRF is a bit lower value for us but we like to keep in enabled.

I've switched to the :reset_session behaviour on any unauthenticated endpoints such as creating a new user or logging in which seems sensible and doesn't cause any problems.

Urgh, looks like we're seeing this on https://petition.parliament.uk. However it looks like we're seeing it across multiple browsers and not limited to Mobile Safari.

We were seeing a lot of InvalidAuthenticityToken exceptions. After reading this thread and further inspection I saw that the exceptions were only caused by mobile browsers. Adding 'Cache-Control' => 'no-store, no-cache' seems to have fixed it. Immediate drop in exceptions. Is this something we always have to account for or is there a better solution?

@Bramjetten That means no browser caching, though. Using a persistent cookie for your session is prob a more reasonable fix for you.

@jeremy That's what I did at first, but it didn't seem to help.

May need to give it some time as people establish new sessions?
On Mon, Apr 4, 2016 at 09:27 Bram Jetten [email protected] wrote:

@jeremy https://github.com/jeremy That's what I did at first, but it
didn't seem to help.


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
https://github.com/rails/rails/issues/21948#issuecomment-205377261

I've managed to get reproduction steps on https://petition.parliament.uk:

  1. Fill out the first page of the signing form
  2. Submit the form to get to the second page where we check your email address
  3. Close the browser
  4. Double tap the home button and force quit Mobile Safari by sliding it off
  5. Re-open Mobile Safari and confirm you want to resubmit the form
  6. Boom! 422 Error

It's the resubmitting of the form that causes the error because the CSRF token is now invalid due to Mobile Safari having deleted the browser session cookie. AFAICS this isn't a bug with Mobile Safari - it's honouring the cache control headers perfectly. The question is whether should it be allowing users to resubmit forms from one browser session to another - maybe even a security bug, but I could understand why because of the way iOS evicts apps when running low on memory.

Note, however, that this may result in usability concerns. For example, the "Back" button browser capability is often hindered as the previous page may contain a token that is no longer valid. Interaction with this previous page will result in a CSRF false positive security event at the server.

That seems to be the main issue for us. +1 on storing the token in a separate permanent cookie.

We had this problem and the suggestion made by @zetter:
config.action_dispatch.default_headers.merge!('Cache-Control' => 'no-store, no-cache')
made by @zetter changes iPhone Safari behaviour (looking good so far..)

I am new to rails, learning by Michael Hartl's RoR Tutorial, I have the similar issue, got 422 when try to login with CSRF error at iPad 9.3.2 Safari, iPad Firefox, iPhone 9.1 Safari, but, running good at Windows 8.1 Chrome, Mac 10.11.5 Safari and Chrome. adding one of these

config.action_dispatch.default_headers.merge!('Cache-Control' => 'no-store, no-cache')
config.action_dispatch.default_headers.merge!('Cache-Control' => 'no-cache')

didn't fix this issue.

I can confirm that it still happens on:

  • OS X 10.11.5 (El Capitan)
  • Safari Desktop 9.1.1 (11601.6.17)
  • Ruby 2.3.1
  • Rails 4.2.6

In order to reproduce, I used the first message in this thread.

For me also getting same error in iPad safari browser. But If I used protect_from_forgery with: :null_session this will not throw an exception, But it shows warning in log that Can't verify token. And I think its not good, any thoughts?

We've got the same issue with Rails 5.0.0. Forms are only accepted on Firefox, not on Chrome and Safari.

Same here on Safari Desktop.

Any news on this?

@aurels I'm wondering if the problem you're having is related to a new default setting in rails 5 - request origin checking, see this check here https://github.com/rails/rails/blob/60c6b538170ce35cc8ff8382bef2f082868b4b09/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L398

Depending on your proxy setup request.base_url could show up as something like localhost:5000 and guarantee a failure for legitimate requests. The reason this doesn't seem to trigger on other browsers (I tested Firefox, IE Edge, Lynx and Links) is that they don't seem to set the Origin header - so Rails skips the same origin check.

You can disable this check with Rails.application.config.action_controller.forgery_protection_origin_check = false - it seems to be a new rails 5 default for it to be true. But it would probably be a better idea to configure your proxy to pass the hostname to rails in the proxy request - I'm not sure how best to do this however.

It would be good for there to be more documentation on this considering it's now on by default in Rails 5 - and could potentially cause a lot of breakage and debugging frustration, though I assume it is only on for new Rails 5 projects, not ones upgraded from Rails 4.

Ah I see there is already a pull request open related to the origin checking I mentioned https://github.com/rails/rails/issues/23905

I probably should have opened a new issue as this is unrelated to @zetter's original report. If doing so would be helpful please let me know.

@daniel-ferguson Thanks, I'll check.

That was indeed that @daniel-ferguson ;-)

Not to plus one, but same issue with Safari on Desktop and Mobile (Rails 4.2.7). Changing to something like

  protect_from_request :reset_session

  def handle_unverified_request
    super
    flash.now[:warning] = 'Your session has expired, please log in again'
  end

works, but it's not pretty. Any thoughts?

@daniel-ferguson, thanks this fixed it for me aswell. spent hours debugging 👍

I am also having this problem, and it's reproducible on Safari desktop as well:

  • In my affected Rails app, open a new form and fill it out.
  • Before submit, copy the url from Safari and close the entire application.
  • Open Safari again, and paste the url - Fill the form again and hit submit. ->
    Response: "Can't verify CSRF token authenticity"

Is this the same issue as you guys are experiencing, or is this my app-specific ?

Any update on this issue? Recently upgraded my application from Rails 3.2 to Rails 5.0 and this error seems to be popping up randomly on different unrelated requests.

As far as I can see, most of these exceptions occur for mobile users but the occasional desktop user agent is represented as well.

Each exception is accompanied by a session content looking like this:

* session id: [FILTERED]
* data: {"session_id"=>"155fbdb38e[FILTERED]70948506",
 "_csrf_token"=>"Kk8QsARU0Al[FILTERED]43LfZR3f6Q="}

Tried the fix of @daniel-ferguson, but this doesn't seem to stop the flow of these exceptions.

@cdekker I suspect you're seeing the issue that I was getting on petition.parliament.uk - the user posts a form and quits the browser and when they reopen the browser it offers to reload the page and when the user clicks yes they get the error because the session cookie is no longer there.

I fixed it by making the session cookie persist for 2 weeks - whether that's acceptable to you on security grounds is a call you have to make. The long term solution for this would be to move the CSRF token out of the session and into a persistent cookie like Django does.

@pixeltrix Thanks for the clarification. I would think a longer session store is a security implication I can live with in my application, especially since most users tick the 'remember me' box anyway, having them log in automatically from a long term cookie.

What I am wondering, though, why is this suddenly currently an issue? Rails 3.2 already had CSRF protection and from what I can tell nothing between Rails 3.2 and Rails 5 changed anything about the client side session cookie store. Why does this issue suddenly generate ugly user-unfriendly error pages and exception mails.

@cdekker because in Rails 4.0 the default was changed from resetting the session to raising an exception for security reasons - the change was made in #5326.

@pixeltrix To me, from a conceptual point of view, every (uncaught) exception that generates an email indicates a flaw, problem or bug in my code that needs to be fixed.

Since this issue is now generating exceptions since Rails 4.0, I wonder how I should 'fix' this. Aparently leaving it uncaught is not the way to go in production. Should I catch this error and redirect to a login page? How does this interfere with the 'remember me' / 'login from cookie' functionality?

I am having trouble deciding the right course of action, regardless of security implication. Surely it cannot be that a default in Rails 4.0 is changed and the only valid course of actions are workarounds like longer cookie expiration?

@cdekker if you wish to revert to 3.2 behaviour you simply do this:

class ApplicationController < ActionController::Base
  protect_from_forgery with: :reset_session
end

Another option if it's mainly caused by expired pages protected by a login would be to capture the exceptions and redirect to the login page.

What that means for your app I can't say without looking at the code. Not every exception is a flaw - for example something raising ActiveRecord::RecordNotFound results in a 404 Not Found page but that's perfectly acceptable. The error your getting is a 422 which is in the 4xx client error range - officially 'their fault' 😄

More seriously, often in these scenarios there isn't a 'right' solution. We could move the CSRF token to a persistent cookie but that's not with pitfalls. Then there's the tricky issue of dealing with CSRF when you're delivering pages via a CDN - encoding them in the page is obviously not going to work and the same goes for a persistent cookie unless you're passing them via your CDN. In this scenario you may eschew CSRF altogether if the form isn't inside a protected area.

HTH

@zetter thanks for a nice explanation of the bug. Here's my monkey patch for this:

https://gist.github.com/killthekitten/b9a7b11530c44e788a31ec53e5ef0dad

It allows you to seamlessly set a persistent cookie with a CSRF token and remove the patch when the community will come up with a decent native fix. I would gladly work on merging it into rails.

I consider the no-cache solution harmful to user experience, this will help to avoid it.

Simply use nullify_session which doesnt raise an exception. CSRF protections only exist to protect the session cookie, to stop other pages doing requests on your behald. If the session is dead, there is nothing to protect.

I recommended using nullify option throughout all your projects before. Because it is most sane and unobstructive approach. No token - no session.
Too bad it is not by defailt...

You then hide any issues arising from invalid session tokens.

@homakov the problem as I see it with :null_session is that you have zero visibility into false positives.

In this case (mobile Safari caches page but not session) most of us have just been blissfully unaware of how many users actually are affected, and how many forms were left unsubmitted.

As long as you can trust that the tokens are properly in place and everything is working, :null_session indeed seems like a good choice. When something goes wrong in the setup, however, it makes it really hard to even notice.

Also, :null_session currently _is_ the default.

@lime you are right, but the problems are direct result of bad practices. If say Devise did remembering via making session persistant instead of creating remember token, csrf token would remain the same.

Default value in the template is exception. Null session is just default arg of protect from forgery.

With null sess users wouldnt see a scary exception but get same page with proper token again neverthheless. Exceptions are for developers not the users!

@mikebaldry csrf exceptions are no different from notorious "undefined is not a function". We do not show it to our customers, do we? It is our dev problem.

@homakov What a user expects and wants to see and what they do see become 2 different things. Leaving them with a bad experience and you none the wiser that there is a problem. THAT is the issue.

Hiding it from yourself and your customers (indirectly) is not a solution, but a band aid.

Who said hiding? You get the message in your log / error tracker. User gets same page reloaded now with valid token. Everybody's happy.

User painstakingly fills in 20 field form, presses submit, gets shown an empty for with no indication that it has or has not been submitted. It will appear in logs maybe, but you won't get it in an error tracker because no error is raised.

And either way, it does not fix the problem, only makes it slightly less of an issue

Form can routinely be prefilled with submitted values. I believe a regular flash.error does it same way already.

I believe there is a way to send an error to tracker without showing 500 to the user. So, it will get fixed in timely fashion too.

That's how null-session should work, not how it is working now.

@killthekitten Would it make sense for this to be a PR against rails itself rather than a monkeypatch? IMO, the "Django" behavior of storing the CSRF token in its own permanent cookie is the correct (and only) fix for this problem.

Edit: this is in reference to the fix posted here https://gist.github.com/killthekitten/b9a7b11530c44e788a31ec53e5ef0dad by killthekitten

@lunaru I've already done that, the PR is waiting to get some attention to it, you can find a link to it between the comment blocks (https://github.com/rails/rails/pull/27689). Sorry that I haven't mentioned it here before.

However, one can argue if it is a good approach, maybe null_session pattern could work better. @homakov has some counter arguments on how it could affect security, but in general, it works for me.

@killthekitten I tried monkey patch written by you and it wasn't helpful in my case. The weird thing is that it sometimes work perfectly fine and other time it just throws the "Can't verify CSRF token authenticity." error message.

One more fact that when I try to inspect issue with byebug, I get all the submitted fields. And, when I remove byebug, then I don't get submitted form's params.

Any help would be really appreciated.

Thanks

@shashi-we there can be multiple reasons for it, my patch fixes only one – described by @zetter in the actual issue.

Under what circumstances do you get your errors?

@killthekitten Thank you for prompt response.

I got this error randomly when I submit my form. Actually, this form create multiple records for a single model. Sometime, I got all the submitted values but sometimes there is nothing in params except 'locale'. authentication_token is also getting generated with this form.

For me, I didn't do any fancy with this form, it just seems general to me like other forms in application.

I am using Rails 5 and Puma.

@shashi-we do you have any JS working on the same page? Do you close the window before the bug happens? Can you provide a sample app to reproduce it in isolation?

@killthekitten ah yes, using JS to update particular form field. Could that be the issue? I am submitting form using JS after updating that field's value.

@shashi-we I assume it's possible that the ajax query refreshes your cookies before you submit the form, therefore at the time you submit it, you do so with a new cookie but an old token in the form itself.

I can confirm that it still happens on:

  • Ruby 2.2.1p85 (2015-02-26 revision 49769) [x86_64-linux]
  • Rails 4.2.0
  • Access log:

Mozilla/5.0 (iPhone; CPU iPhone OS 10_2_1 like Mac OS X) AppleWebKit/602.4.6 (KHTML, like Gecko) Version/10.0 Mobile/14D27 Safari/602.1

  • Error log:
Can't verify CSRF token authenticity
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
...

I also can confirm this bug:

ruby 2.2.6p396 (2016-11-15 revision 56800) [x86_64-darwin16]
Rails 5.0.2
Chrome 57.0.2987.110 (64-bit)

Same here.

ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux]
Rails 5.0.3

The app is working like a charm on Firefox (53.0.3-64bit).
Can't submit a form on Chromium (58.0.3029.110-64bit).

Already tried all of the above, without success so far.

Same here.

ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-linux]
Rails 5.0.3
Forms are generated by RailsAdmin either 1.1.1 or 1.2.0

The app is working on anything except for Safari on Mac (iOS version is working).
On Safari the clients get ActionController::InvalidAuthenticityToken every time they submit a POST form.
Safari versions are 10.1 (11603.1.30.0.34) and 10.1.1 (12603.2.4).

I've tried to change session_store to :cookie_store from :redis_store,
tried to log users out and clear session[:_csrf_token] in the rescue_from ActionController::InvalidAuthenticityToken block.
No good.

Eventually, I deployed the code with the CSRF protection on another server, and my Chrome (versions 56—59) started to get the errors, too. So I ended up implementing the Cookie-based transmission of CSRF tokens from here: https://www.fastly.com/blog/caching-uncacheable-csrf-security/

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action do
    cookies[:csrftoken] = {value: form_authenticity_token, secure: true}
  end
end
jQuery(function() {
  // using js-cookie
  $('meta[name=csrf-token]').attr('content', Cookies.get('csrftoken'));
  $('input[name=authenticity_token]').val(Cookies.get('csrftoken'));
})

Update:

After a few days the solution also broke and I had to disable the protection in our CMS app completely.
I mean, the token generated by form_authenticity_token during a request doesn't match the token expected by Rails during the very next [POST/PATCH] request.

This MAY be related to load balancer/reverse proxy stack misconfiguration when request.ip != request.remote_ip condition occurs.

Based on @typeoneerror's suggestion, here's how I worked around this issue:

class SessionsController < Devise::SessionsController
  rescue_from ActionController::InvalidAuthenticityToken, with: :warn_session_reset

  private

  def warn_session_reset
    redirect_back(
      fallback_location: root_path,
      alert: 'We had to block your login attempt because your session has expired. Please try again.'
    )
  end
end

Note that I'm only doing this in the sessions controller because that's where most people seem to be experiencing the error. It's not a perfect solution but it works for me so far.

Also, frankly, I don't care that it shows a "wrong" error message to real CSRF attacks: It prevents access, so it does its job.

@clemens this seems to be a good fit for me for now. I'm not sure why rails does not reset session on csrf token verification failure and raises an exception by default.

This issue has been automatically marked as stale because it has not been commented on for at least three months.
The resources of the Rails team are limited, and so we are asking for your help.
If you can still reproduce this error on the 5-1-stable branch or on master, please reply with all of the information you have about it in order to keep the issue open.
Thank you for all your contributions.

To sum it up: it didn't stop failing after we deployed the monkey patch presented in my PR (#27689) but it did prevent some class of failures.

There could be many sources of these errors, and mostly, I think, that's related to your client JS. I.e. after removing several AJAX calls from the front-end, the number of errors has significantly reduced.

We decided not to spend more time and leave it as it is.

Can confirm that this is still an issue on Rails 5.1.4.

Deployed an application for the first time on Friday, and have received 7 ActionController::InvalidAuthenticityToken exception reports so far. Most seem to be webkit based mobile browsers (some iOS, some Android), but there are a couple of Deskop Mozilla ones in there:

* HTTP Method: POST
* HTTP_USER_AGENT : Mozilla/5.0 (Linux; Android 8.1.0; Nexus 5X Build/OPP6.171019.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36

---

* HTTP Method: POST
* HTTP_USER_AGENT : Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:57.0) Gecko/20100101 Firefox/57.0

---

* HTTP Method: POST
* HTTP_USER_AGENT : Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD3.170816.023) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.71 Mobile Safari/537.36

---

* HTTP Method: POST
* HTTP_USER_AGENT : Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_1 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0 Mobile/15B150 Safari/604.1

---

* HTTP Method: POST
* HTTP_USER_AGENT : Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Safari/537.36

---

* HTTP Method: PATCH
* HTTP_USER_AGENT : Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_1 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0 Mobile/15B150 Safari/604.1

---

* HTTP Method: PATCH
* HTTP_USER_AGENT : Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_1 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0 Mobile/15B150 Safari/604.1

Worth noting that from looking at the logs, the timing of almost all of them would seem to suggest that people probably aren't closing browsers or anything like that - something else is going on.

something else is going on
Likely they are using the back button

On Sat, Dec 2, 2017, 23:26 John McDowall notifications@github.com wrote:

Can confirm that this is still an issue on Rails 5.1.4.

Deployed an application for the first time on Friday, and have received 7
ActionController::InvalidAuthenticityToken exception reports so far. Most
seem to be webkit based mobile browsers (some iOS, some Android), but there
are a couple of Deskop Mozilla ones in there:

  • HTTP Method: POST
  • HTTP_USER_AGENT : Mozilla/5.0 (Linux; Android 8.1.0; Nexus 5X Build/OPP6.171019.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36

  • HTTP Method: POST
  • HTTP_USER_AGENT : Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:57.0) Gecko/20100101 Firefox/57.0

  • HTTP Method: POST
  • HTTP_USER_AGENT : Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD3.170816.023) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.71 Mobile Safari/537.36

  • HTTP Method: POST
  • HTTP_USER_AGENT : Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_1 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0 Mobile/15B150 Safari/604.1

  • HTTP Method: POST
  • HTTP_USER_AGENT : Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Safari/537.36

  • HTTP Method: PATCH
  • HTTP_USER_AGENT : Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_1 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0 Mobile/15B150 Safari/604.1

  • HTTP Method: PATCH
  • HTTP_USER_AGENT : Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_1 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0 Mobile/15B150 Safari/604.1

Worth noting that from looking at the logs, the timing of almost all of
them would seem to suggest that people probably aren't closing browsers
or anything like that - something else is going on.


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/rails/rails/issues/21948#issuecomment-348739405, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAS_77dk3-B7v7GO-QSkp3HOnqQbqWsNks5s8iLfgaJpZM4GNsnP
.

This issue has been automatically marked as stale because it has not been commented on for at least three months.
The resources of the Rails team are limited, and so we are asking for your help.
If you can still reproduce this error on the 5-1-stable branch or on master, please reply with all of the information you have about it in order to keep the issue open.
Thank you for all your contributions.

Still happening in 5.1.5. Haven't checked against master/5.2.

I've been having this issue with devise signin; not sure what's going on, locally everything works fine but on my staging server login doesn't work.

update: the fix I found was to change the protect_from_forgery call to this in the application controller:

protect_from_forgery prepend: true

At our Rails shop we have a handful of web applications of various setups, all updated to Rails 5.1.6 and using protect_from_forgery with: exception in application_controller.rb. I think most are using cookie session store right now.
All of our applications are suffering from this issue.

I believe I have found a more generic, easily reproducible version of this bug (applicable to sites with login pages/session-tracked users). This doesn't seem to be browser or technology specific in any way. It doesn't require closing the browser either.

Steps to reproduce:

  • Open two browser tabs and navigate both to the application login page. (form protected with CSRF)
  • Log into the application using the first tab.
  • Log out of the application using the first tab. (destroys the session)
  • Attempt to log into application again using the second tab. (stale CSRF token can't match with non-existent session)

You will be presented with the InvalidAuthenticityToken error in the original post.

Interestingly enough, trying this for GitHub itself (ignoring the warning at the top about needing to refresh the page) seems to invoke the error, but the user is simply shown a static page, appearing as if the login attempt is taking a very long time to complete. The login attempt never completes.

I can confirm this, my app is breaking in Chrome while used on production. I use Rails 5.2

Same as @rudolfolah, I had to use protect_from_forgery with: :exception, prepend: true to fix the issue.

This new behavior is present since Rails 5.0 and has been added by this commit: https://github.com/rails/rails/commit/39794037817703575c35a75f1961b01b83791191

See also Devise README: https://github.com/plataformatec/devise/blob/715192a7709a4c02127afb067e66230061b82cf2/README.md#controller-filters-and-helpers

I can confirm that it still happens with a brand new app:

  • Ruby 2.5.1
  • Rails 5.2.1
  • Safari 11.1.2 (13605.3.8)
  • macOS 10.13.6 (High Sierra)

hi @maclover7 - can you please remove the needs feedback label?

This issue has been automatically marked as stale because it has not been commented on for at least three months.
The resources of the Rails team are limited, and so we are asking for your help.
If you can still reproduce this error on the 5-2-stable branch or on master, please reply with all of the information you have about it in order to keep the issue open.
Thank you for all your contributions.

The bot auto-closed this issue, but I'd say it's still relevant as I've noticed an upswing in these exceptions as more traffic goes to mobile devices.

Seems like the best solution for a lot of apps is the Cache-Control suggestion above combined with redirecting the user to the root_path along with a helpful message suggesting they retry when CSRF does fail. The persistent cookie idea is interesting, but appears to have its own issues (?)

Maybe just a matter of updating the docs and suggesting people add cache control and some handling if they're going to use the :exception style of CSRF enforcement?

I can confirm the original issue by @zetter is still relevant and can be reproduced easily.

This needs to be re-opened.

cc @rails-bot

I have the same issue. On every webkit browser I try, not in other browsers.

Hey @rafaelfranca, could you please shed some light?
Sorry if you aren't the right person to @ in this case.

Thank you.

If you need extremely easy steps to reproduce on any browser and any Rails site using protect_from_forgery with: exception, including GitHub itself, check my previous post:
https://github.com/rails/rails/issues/21948#issuecomment-389310516

All of our sites are on Rails 5.2.2 now, so I can check there, but it's going to be the same story.

@BenFenner out of curiosity what would you expect it to do in your example? AFAICS there are two options:

1) Ignore the CSRF token error or make the token valid somehow and process the request
2) Redirect to the login page

Option 1 is a massive security hole so that just leaves option 2 which is what you'll get when you change protect_from_forgery to use :null_session.

The problem with approach #2 is that these users may not expect to have been logged out of the site and will get annoyed that they're frequently being asked to log in again. A third option exists: redirect to home with a message asking that they retry their request. This gets the token refreshed, prevents a potentially insecure action, and allows the user to continue without having to re-login.

@rdlugosz that's an app-level concern, not something we can do at the framework level, e.g.

class PostsController < ApplicationController
  rescue_from ActionController::InvalidAuthenticityToken do
    redirect_to root_url, alert: "Your request has expired, please try again"
  end
end

I think the best we can do to fix this is to add a section to the security guide on the causes and how you can deal with the problem.

It seems that there may be some approaches to deal with this that might feel better to the user, for example:

1) use an approach that logs out the user in all tabs when logged out in one (achievable with websockets, localstorage events)
2) update CSRF tokens or refresh the session in all tabs when session changes (again with websockets or localstorage events).

But these solutions feel probably beyond the scope of this issue as the exact desired behavior might be application specific. It would be helpful to at least describe this issue in the security guide so that developers stumbling upon this can understand what's happening and make an informed choice.

Does this issue exist only when users are logging in/out and they have multiple tabs open, or are there other scenarios it could happen?

I agree that updating the docs may be the best resolution to this, failing the proper vetting and implementation of a persistent cookie like Django uses as described earlier (which I'm not qualified to do; that needs careful attention from the sec team).

One issue in this thread is that two different scenarios have gotten co-mingled. The original issue has to do with mobile browsers not persisting the CSRF state between extended sessions (i.e., I have a tab open in safari on my iphone, leave it for a while, and then when I come back the next search I submit to the app causes an invalid token error). Later, a separate scenario was described that involves multiple tabs and logging out in one of them... that's an entirely different situation than what started this issue.

So @travisp, no - it isn't really about multiple tabs, although there may be a related case that involves them.

We can confirm that at least one of our users is having this same issue right now. We haven't yet been able to solve it.

Hi, I've been following this issue for quite some time now, and I'm just wondering.

To developers suffering from this issue, does your project use Devise for authentication?

Please react to this comment with :+1: for yes, :-1: for no.

A heads-up to someone trying to find out why current_user is being nil inside your Devise::RegistrationsController even though it calls autheticate_user! behind the scenes for actions like update.

Basically, I create guest accounts for users to try out my service (I have an attribute role=guest in my User model). After some time, I display a form suggesting users to "save" their account by entering an email and a password. For years I couldn't understand how this code would result in NoMethodError: undefined methodid' for nil:NilClass`:

class Users::RegistrationsController < Devise::RegistrationsController
  def update
    @user = User.find(current_user.id)
    # Update @user from role=guest to role=user, etc
  end
end

So it turns out, here's what caused the error:

  1. I create a guest account for a user.
  2. Some time later I display a form and ask the user to enter their email & password.
  3. At this step, the user closes/switches from their iOS browser to another app.
  4. Some time later, the user opens their iOS browser again, sees the old form and tries entering email & password and submitting it.
  5. It results in an invalid CSRF token, which results in current_user being nil inside the update action. Also, this resulted in the user's session being reset, resulting in a loss of the their guest account & data they entered.

The fix I'm using now:
Raise an exception in ApplicationController

class ApplicationController < ActionController::Base
  protect_from_forgery prepend: true, with: :exception
end

And rescue it inside Users::RegistrationsController

class Users::RegistrationsController < Devise::RegistrationsController
  rescue_from ActionController::InvalidAuthenticityToken do
    redirect_to request.referrer, alert: "Your request has expired, please try again"
  end
end

This way, the user stays logged in, sees "Your request has expired, please try again" and is presented with a fresh form with a valid csrf token.

It took me a few years to hunt down this bug until I got fed up with it today :) Hopefully this info will help someone with a similar problem.

So it turns out that Firefox may autocomplete hidden fields:

https://bugzilla.mozilla.org/show_bug.cgi?id=520561

Has anyone seen this as a cause of 422 errors? For example someone could log out in one tab and then hit refresh in another which may result in an stale authenticity_token being injected into the hidden field.

It's not a Rails problem if you are using _nginx_, _puma_ along with _certbot_. Its happening because certain stuff to authenticate is not available for the Rails app, before reaching Rails it gets filtered out.

To fix it you can see this https://stackoverflow.com/questions/39012356/devise-cant-verify-csrf-token-authenticity-the-https-enabled-on-server-no-jso

I've recently had a huge spike in ActionController::InvalidAuthenticityToken errors due to what I believe is browser HTML caching resulting in requests without the session cookie (as described in this thread).

I'm testing a solution that:

  1. Sets the XSRF-TOKEN as an unsigned cookie as described in this SO answer by u/HungYuHei. Relevant code:
# application_controller.rb
class ApplicationController < ActionController::Base
  after_action :set_csrf_cookie_for_ng

  private

  # 
  def set_csrf_cookie_for_ng
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  end

  # Optional. Described more below.
  # Extend valid_authenticity_token? to also check XSRF-TOKEN
  def verified_request?
    super || valid_authenticity_token?(session, request.headers['X-XSRF-TOKEN'])
  end
end
  1. Uses some javascript to check whether the above cookie is set. The above cookie along with the session cookie will be dropped if the user closes the browser. Since the session cookie is HTTP only we can't read it with javascript. However we can read XSRF-TOKEN that we set and if it's not there we can force a browser refresh:
// app/javascript/packs/application.js
const hasCrossSiteReferenceToken = () => document.cookie.indexOf('XSRF-TOKEN') > -1;

if (!hasCrossSiteReferenceToken()) {
    location.reload();
}

A few notes:

  1. I'm definitely concerned about DOS'ing my own server here. But so far, it hasn't happened. I can't see a way that it would.
  2. You don't have to set a XSRF-TOKEN in the cookie... it could be anything (timestamp, for example). Just make sure it's set with every non-protected request to your site.

Would welcome comments.

I just launched a new Rails v6.0.2.2 application a month ago with a 85% mobile user base and I get a consistent bunch of InvalidAuthenticityTokens errors every day (I will say less than 1% of all requests anyway) but bothers me for the UX of these people.

Just finished reading all thread looking for mitigations. This issue is still relevant 5 years later!

Interesting topic.

I agree that the issue is still relevant.

I now understand better in which situation does the issue arises. I'm no security guru, and still am not very clear on what situation does the csrf token prevents attacks but I understand this has to do with current page freshness, hasn't it?

Couldn't we imagine something like another hidden field, along the x-csrf-token one, that would be token-generated-at: timestamp, and a rails_ujs a script that does like:

$(window).on('focus', e => {
  if $('[name=token-generated-at]').val() > specific duration {
    window.location.reload(false);
  }
});

Or if this has to do with the presence of a session cookie, this should be also checkable with JS, shouldn't it?

This would be a lesser optimized UX for those users but at least they wouldn't have any error!

Was this page helpful?
0 / 5 - 0 ratings