New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CSRF protection prevents some webkit users from submitting forms #21948
Comments
I can't see any way to prevent this happening without opening the application to possible CSRF attacks. |
@zetter does changing the |
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 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. |
An update:
@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 DjangoRails adds the token to the session cookie under the Django puts adds the token in it's own cookie called Why does Rails behave in this way?Rail's CSRF protection comes from the
This warning got imported into Rails from the
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:
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:
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 |
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 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:
|
@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? |
You can try that by giving your session cookie an 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 This sort of attack doesn't affect Rails applications because the CSRF token is stored in a signed cookie. Most applications will 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. |
in Mobile Safari and Mobile Chrome Ref rails/rails#21948 (comment)
I've been seeing something similar since switching to the I've switched to the |
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?
|
Mobile Safari has a tendency to use cached form values even when the cache control headers tell it otherwise. However the session cookie has expired so when the form is submitted the CSRF token is invalid. See rails/rails#21948 for further details. Fixes #451.
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. |
Due to some issues with Rails sessions expiry time and browsers restoring open pages/tabs with stale CSRF tokens but expired sessions (see rails/rails#21948), posting to /authorization fails for some users due to failed CSRF checks. Since this endpoint simply redirects to initiate the Google OAuth flow, there's nothing to be compromised by forging that request. It's similar to disabling CSRF protection on login.
Due to some issues with Rails sessions expiry time and browsers restoring open pages/tabs with stale CSRF tokens but expired sessions (see rails/rails#21948), posting to /authorization fails for some users due to failed CSRF checks. Since this endpoint simply redirects to initiate the Google OAuth flow, there's nothing to be compromised by forging that request. It's similar to disabling CSRF protection on login.
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. |
Due to some issues with Rails sessions expiry time and browsers restoring open pages/tabs with stale CSRF tokens but expired sessions (see rails/rails#21948), posting to /authorization fails for some users due to failed CSRF checks. Since this endpoint simply redirects to initiate the Google OAuth flow, there's nothing to be compromised by forging that request. It's similar to disabling CSRF protection on login.
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 |
A heads-up to someone trying to find out why Basically, I create guest accounts for users to try out my service (I have an attribute 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:
The fix I'm using now: class ApplicationController < ActionController::Base
protect_from_forgery prepend: true, with: :exception
end And rescue it inside 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 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 |
Some browsers have a behaviour where although they will delete a session cookie when the app is shutdown, they will still serve a cached version of the page on relaunch. This causes issues with CSRF protection as it relies on the token embedded in the page/form matching the token in the session cookie. So a user that then submits the form will get a 422 error (`ActionController::InvalidAuthenticityToken`) because their is nothing in the session. We get around this by giving the session cookie a life beyond the browser session. We can do this without risk of exposing user data as we still explicitly expire a active claim and admin sessions based on the `last_seen_at` timestamp. See rails/rails#21948 for further details on the issue.
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:
# 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
// app/javascript/packs/application.js
const hasCrossSiteReferenceToken = () => document.cookie.indexOf('XSRF-TOKEN') > -1;
if (!hasCrossSiteReferenceToken()) {
location.reload();
} A few notes:
Would welcome comments. |
This instruct browsers to never cache content directly generated by the controllers. This includes HTML pages, JSON responses, PDF files, etc. This is because Some mobile browsers have a behaviour where, although they will delete the session cookie when the browser shutdowns, they will still serve a cached version of the page on relaunch. The CSRF token in the HTML is then mismatched with the CSRF token in the session cookie (because the session cookie has been cleared). This causes form submissions to fail with an "ActionController::InvalidAuthenticityToken" exception. To prevent this, tell browsers to never cache the HTML of a page. (This doesn’t affect assets files, which are still sent with the proper cache headers). See rails/rails#21948
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
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! |
Some browsers have a behaviour where although they will delete a session cookie when the app is shutdown, they will still serve a cached version of the page on relaunch. This causes issues with CSRF protection as it relies on the token embedded in the page/form matching the token in the session cookie. So a user that then submits the form will get a 422 error (`ActionController::InvalidAuthenticityToken`) because their is nothing in the session. We get around this by giving the session cookie a life beyond the browser session. See rails/rails#21948 for further details on the issue.
See GHSA-h3fg-h5v3-vf8m for all the details. Some time ago, all order actions were left out of CSRF protection (see 95ea570). The reason given was that the authentication token got stale after the second rendering because the product page is cached. That was limited to `#populate` in cb79754 (see also spree/spree#5601). However, those assumptions are not correct. Although the authenticity token changes at every request, that doesn't mean that the old ones are no longer valid. The variation comes from a one-time pad added to a session-dependant token (and meant to avoid timing attacks). However, before validation, that one-time pad is removed. That means the token remains valid as long as the session has not been reset. Think about submitting a form from one browser tab after opening another with the same URL. Even if both tokens differ, the submission from the first tab will still be valid. You can read https://medium.com/rubyinside/a-deep-dive-into-csrf-protection-in-rails-19fa0a42c0ef for an in-deep understanding. The initial confusion could come because of rails/rails#21948. Due to browser-side cache, a form can be re-rendered and sent without any attached request cookie. That will cause an authentication error, as the sent token won't match with the one in the session (none in this case). There's no perfect solution for that, and all partial fixes should be seen at the application level. From our side, we must provide a safe default. For an excellent survey of all the available options, take a look at https://github.com/betagouv/demarches-simplifiees.fr/blob/5b4f7f9ae9eaf0ac94008b62f7047e4714626cf9/doc/adr-csrf-forgery.md. The information given in that link is third-party but it's very relevant here. For that reason we've copied it in the security advisory (see link above), but all the credit goes to @kemenaran.
See GHSA-h3fg-h5v3-vf8m for all the details. Some time ago, all order actions were left out of CSRF protection (see 95ea570). The reason given was that the authentication token got stale after the second rendering because the product page is cached. That was limited to `#populate` in cb79754 (see also spree/spree#5601). However, those assumptions are not correct. Although the authenticity token changes at every request, that doesn't mean that the old ones are no longer valid. The variation comes from a one-time pad added to a session-dependant token (and meant to avoid timing attacks). However, before validation, that one-time pad is removed. That means the token remains valid as long as the session has not been reset. Think about submitting a form from one browser tab after opening another with the same URL. Even if both tokens differ, the submission from the first tab will still be valid. You can read https://medium.com/rubyinside/a-deep-dive-into-csrf-protection-in-rails-19fa0a42c0ef for an in-deep understanding. The initial confusion could come because of rails/rails#21948. Due to browser-side cache, a form can be re-rendered and sent without any attached request cookie. That will cause an authentication error, as the sent token won't match with the one in the session (none in this case). There's no perfect solution for that, and all partial fixes should be seen at the application level. From our side, we must provide a safe default. For an excellent survey of all the available options, take a look at https://github.com/betagouv/demarches-simplifiees.fr/blob/5b4f7f9ae9eaf0ac94008b62f7047e4714626cf9/doc/adr-csrf-forgery.md. The information given in that link is third-party but it's very relevant here. For that reason we've copied it in the security advisory (see link above), but all the credit goes to @kemenaran.
#44283 may help with this, as the new However it seems that by default the |
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:
How to replicate it on mobile Safari (tested on iOS9):
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)
This problem seems to happen regardless if:
development
orproduction
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:
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:
Thanks for any help.
The text was updated successfully, but these errors were encountered: