In our Rails application we use protect_from_forgery
to prevent CSRF.
However what we found is that if a user visits the login page and then goes off to make a cup of tea for the duration of what the app session expiration time is (let's say 15 minutes). The login just redirects back to the login page regardless of successful credentials or not (Depending on the environment, we get the error InvalidAuthenticityToken
.) Trying to login again works fine. It only fails if the user has been on the page for longer than the session time.
We thought this was weird because we haven't logged in yet... so what session is expiring? and surely a new session is being created on login even if one was created and had expired. Turns out (after reading this: https://nvisium.com/blog/2014/09/10/understanding-protectfromforgery/) the CSRF protection in Rails actually uses a session to check the authenticity_token
is valid. So basically the token expires when the session expires (depending on session_store
setting), and you can't login without refreshing the page again.
We solved this doing: skip_before_action :verify_authenticity_token, only: [:create]
in our SessionsController
but now that means our login form is no longer protected.
What other options are there to fix this? Or is the solution we have used not as insecure as we think? Googling shows this line of code used lots of times, but surely it's a bad idea?
Our other solution was to allow the Exception to happen, but handle it gracefully with:
rescue_from ActionController::InvalidAuthenticityToken do
@exception = exception.message
render 'errors/500', :status => 500, :layout => 'other'
end
Though still hating the fact a user sitting on the login page for longer than the session timeout (in this case 15 mins) causes an error!
Yet another solution we have come up with is to set the session_store to forever and then manually expire login sessions like this:
before_action :session_timeout, if: :current_user
def session_timeout
session[:last_seen_at] ||= Time.now
if session[:last_seen_at] < 15.minutes.ago
reset_session
else
session[:last_seen_at] = Time.now
end
end
so what session is expiring?
The session in question is the Rails Session (see What are Sessions? in the Ruby on Rails Security Guide), which is a lightweight data-storage abstraction that persists arbitrary state across HTTP requests (in an encrypted cookie by default, other session-storage implementations can also be configured). Session state may include an authenticated user ID, but it can also be used for other purposes.
In this case, the session is not being used for user authentication, but to store a temporary 'authenticity token' as part of a Rails security feature that protects your website against Cross-Site Request Forgery (CSRF) attacks on POST (or other non-GET) requests.
The Rails API documentation describes this feature in more detail:
Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks by including a token in the rendered HTML for your application. This token is stored as a random string in the session, to which an attacker does not have access. When a request reaches your application, Rails verifies the received token with the token in the session.
See also the Cross-Site Request Forgery section of the Ruby on Rails Security Guide for a more complete overview of what a CSRF attack is, and how session-based authenticity-token verification protects against it.
What other options are there to fix this?
Increase the duration of your Rails session expiration time (e.g., by increasing the duration of the expire_after
option passed to your cookie_store
initializer, or removing the option entirely to make the session never expire).
Instead of using session-cookie expiration to expire logged-in sessions, use Devise's :timeoutable
module:
devise :timeoutable, timeout_in: 15.minutes
Or is the solution we have used not as insecure as we think?
Configuring Rails to skip the verify_authenticity_token
callback disables CSRF protection for that particular controller action, which makes the action vulnerable to CSRF attacks.
So to rephrase your question, is disabling CSRF protection only for the SessionsController#create
action still insecure in any significant/meaningful sense? Though this depends on your application, in general yes, I believe so.
Consider this scenario:
A CSRF attack against SessionsController#create
would allow an attacker to maliciously direct a victim's browser to login to a user account under the attacker's control. The next time the victim visited your website (e.g., when redirected by the attacker), their browser could still be logged in to the attacker-controlled account. The victim could then unknowingly submit sensitive personal data or perform sensitive actions on your website that could be read by the attacker.
While this scenario may be less obviously dangerous than what could happen if CSRF protection were disabled on other more sensitive/destructive controller actions, it still exposes enough of a potential user/data-privacy issue that I consider it to be insecure enough to recommend against disabling the default protection.
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