Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 277
Jason FB Avatar asked Jul 26 '17 14:07

Jason FB


People also ask

How does CSRF token work in Rails?

Rails CSRF TokenThe server generates these tokens, links them to the user session, and stores them in the database. This token is then injected into any form presented to the client as a hidden field. When the client correctly submits the form for validation, it passes the token back to the server.

Is CSRF token a session token?

CSRF token is not tied to the user session In this situation, the attacker can log in to the application using their own account, obtain a valid token, and then feed that token to the victim user in their CSRF attack.

Is CSRF token unique per request?

The webserver needs a mechanism to determine whether a legitimate user generated a request via the user's browser to avoid such attacks. A CSRF token helps with this by generating a unique, unpredictable, and secret value by the server-side to be included in the client's HTTP request.


1 Answers

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/

like image 69
Jason FB Avatar answered Oct 06 '22 00:10

Jason FB