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
3 Answers
Answers 1
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 yourcookie_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.
Answers 2
InvalidAuthenticityToken
This error comes when "authenticity token" created while rendering form (login form) is already expired.
There are only two ways by which we can resolve this issue.
- Configuring Rails to skip the verify_authenticity_token for controller#action - Insecure as already mentioned in the question
- (Only left way)Auto reload the screen(here Login screen ) whenever such exception encountered.
Find the modified code for the same and add this in application_controller.rb to generalize the solution for whole application.
rescue_from ActionController::InvalidAuthenticityToken do redirect_to root_url end
Answers 3
Another option would be to include some JS on the page that would automatically reload a little while before your token expires (and therefore keep the auth token valid).
You would probably want to check to check for mouse/keyboard action before reloading in case the user is just in the process of filling out the form.
It seems that the biggest downside of this option would be the fact that the JS is decoupled from the actual amount of time the auth token is valid... So it could prove to be a maintenance issue if you change your timeout duration often.
0 comments:
Post a Comment