How Rails Protects From CSRF Attacks


Since v2.0, Rails has shipped with token-based protection against cross-site request forgery (CSRF) attacks. You may have run into this issue while trying to create and test a form that does not rely on Rails' built in form_for function. If you create a form in pure HTML and submit it to a Rails controller action, by default, it will throw the warning:

WARNING: Can't verify CSRF token authenticity

What does this mean, and how is it fixed?

The quick fix: include a hidden parameter in your form with the name authenticity_token and value that is the output of the form_authenticity_token helper method, like so:

<input type="hidden" name="authenticity_token" value="form_authenticity_token">

This will put a stop to the warnings and protect your application from CSRF attacks. But what is going on here?

A cross-site request forgery attack refers to an attacker using the authentication information of a trusted user in a request. For example, an attacker could steal a cookie containing the authentication information of a user, and then send that cookie to the website with a forged request. In your Rails app, this could take the form of an attacker using a malicious script and a trusted user’s cookies to POST malicious data to some action. The attack might not break any of your application’s functionality - it could be supported but undesired behavior, e.g. making a withdrawal from a bank account.

Rails protects against CSRF attacks by sending a CSRF token with each request. A correct CSRF token must be provided with each request. In fact, if you are using the popular Devise gem, a user who submits a form with an invalid CSRF token, or without a CSRF token, will be logged out.

We can see how this works by looking at the relevant code: the relevant code is in ActionController::RequestForgeryProtection. The definition of the form_authenticity_token method is simple:

  # Sets the token value for the current session.
  def form_authenticity_token
    session[:_csrf_token] ||= SecureRandom.base64(32)

As we can see, Rails is simply generating a random 32-byte number to serve as the CSRF token, once per session. The method that does the actual testing of CSRF tokens is verified_request?:

  # Returns true or false if a request is verified. Checks:
  # * is it a GET or HEAD request?  Gets should be safe and idempotent
  # * Does the form_authenticity_token match the given token value from the params?
  # * Does the X-CSRF-Token header match the form_authenticity_token
  def verified_request?
    !protect_against_forgery? || request.get? || request.head? ||
      form_authenticity_token == form_authenticity_param ||
      form_authenticity_token == request.headers['X-CSRF-Token']

All this method does is check if the CSRF token provided by the request matches the CSRF token stored in memory. This provides a good measure of protection against CSRF attacks, as it requires the attacker to not just intercept a cookie, but also intercept a page from the current session in order to effectively forge a request.

Inspecting verified_request? can teach us about the limitations of token based CSRF protection. Note that verified_request? will always return true for GET and HEAD requests. As the comments note, ‘Gets should be safe an idempotent’. It is essential that Ruby on Rails developers respect the intended meanings of the HTTP verbs. Ignoring the documented uses of the HTTP verbs can introduce security flaws into your application.

Finally, note that Rails uses the same CSRF token for an entire session. Exercise care when sending forms via AJAX - if an attacker is able to intercept a form from the current session, they will have a CSRF token that will work on any form for the user across your application.

Hopefully you now have a better understanding of how CSRF token verification works and just what Rails is doing under the hood.

I'm looking for better ways to build software businesses. Find out if I find something.