Search code examples
ruby-on-railscsrfauthenticity-token

CSRF tokens to not match what is in session (Rails 4.1)


We are seeing an unfortunate and likely browser-based CSRF token authenticity problem in our Rails 4.1 app. We are posting it here to ask the community if others are seeing it too.

Please be aware that most error reporting tools — like Honeybadger — automatically suppress ActionController::InvalidAuthenticityToken, so you don't normally see the problem in your error reporting tool unless you go out of your way to see it.

Here's the problem, and this is NOT a development issue — it is a production issue that has yet to be diagnosed.

The exception we see is simply ActionController::InvalidAuthenticityToken on normal logins to our website. Upon careful examination of the authenticity_token sent by the form and the session's _csrf_token (we are using active_record_store as our session_store setting), they just don't match. Upon direct examination, I can conclude only that they are completely different tokens, but I don't know why.

We see this problem broadly, maybe about 1-2% of our high traffic website. I see it only in Production, I am unable to reproduce it in development whatsoever.

I see it on IE 11 and Edge browsers most (you will note Rails 4.1 was released before IE 11 and Edge), but also on Chrome on Android and occasionally mobile Safari too.

Our Cache-control headers are set as follows:

Cache-Control: max-age=0, private, must-revalidate


Solution

  • This is been identified and fixed. The cache control headers were not set in our Rails 4.1 application, leading to the default headers of

    Cache-Control: max-age=0, private, must-revalidate
    

    This header is not strong enough to force browsers to not cache. Thus, the login form and JSON token were being cached by the client browser — notably mobile clients — and returning session_ids that were expired.

    To fix:

    Set cache-control and pragma header, as such

    Cache-Control:no-cache, no-store, max-age=0, must-revalidate
    

    and

    Pragma: no-cache
    

    IN rails, add this to your application_controller.rb :

    before_action :set_cache_headers
    def set_cache_headers
      response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
      response.headers["Pragma"] = "no-cache"
      response.headers["Expires"] = "Mon, 01 Jan 1990 00:00:00 GMT"
    end
    

    Should it be global to every action in your app? This is up to you, but you will definitely want to do this on any controller that renders a form, particularly a log-in form, or for any page that renders a JSON token which might expire. So in in modern apps, the short answer is yes.

    If you explicitly want to keep your Rails app responses cached you need to figure out how to explicitly expire these CSRF and JSON tokens if embedded.

    Note the symptom manifests at subtle occurrence levels on mostly mobile clients.


    I explored this in a blog post here, please visit my blog and consider leaving a comment there to discuss: https://blog.jasonfleetwoodboldt.com/2017/09/03/the-great-rails-cache-lie/