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
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.
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.
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.
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/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With