Authenticity Tokens in Rails

How Rails Authenticity Tokens Protect Against CSRF Vulnerability

Rails protects your web application from CSRF attack by including an authenticity token in the HTML forms. This token is also stored in the user's session. Upon receiving a request, Rails compares these two tokens to decide if the request is verified.

6 min read

In the previous article, we learned how Cross-Site Request Forgery (CSRF) vulnerability works by tricking the authenticated users into performing a dangerous activity on the application, such as transferring funds or granting access to a protected resource.

In this post, we'll learn how Ruby on Rails helps prevent CSRF attacks using authenticity tokens. It covers the following topics:

Let's get started.

What are Authenticity Tokens in Rails?

If you've been building web apps using Rails even for a while, you must have encountered a hidden input with a strange value in the rendered HTML forms.

<form action="/posts" accept-charset="UTF-8" method="post">
  <input type="hidden" name="authenticity_token" value="n4FHeWc4WBJLi5wU1bBmQH2lIJNKizNmxNgDj9VAD-9OBhVWVbJr1-YbQ2KuvS4T8BBOYhaRTOpgRzqCHoT-hA" autocomplete="off">
</form>

These cryptic values are called authenticity tokens, and they help you prevent CSRF vulnerability.

Rails automatically generates a CSRF token whenever the application requests a form. This token is also stored in the user's session and changes each time the session is regenerated. Hence, a malicious application cannot access it.

When the user submits the form and the request reaches your application, Rails verifies the received token with the token in the session. If the two match, it means the request is verified, and Rails will allow it.

Authenticity Tokens in Rails
Authenticity Tokens in Rails

If the tokens don't match, an ActionController::InvalidAuthenticityToken error is raised, indicating that the request is unverified. This ensures that the authenticated user is the person making the requests to the application.

Rails checks all requests except the GET requests, as these should be idempotent, that is, they should not have any side effects.

How Rails Adds Authenticity Tokens to the Forms

The ActionController::RequestForgeryProtection module (which is a concern) contains the logic related to CSRF protection. It includes methods that generate the tokens and check if the tokens match. This module is included in all Rails controllers via the ActionController::Base class.

Request Forgery Protection
Request Forgery Protection

To learn more about concerns in Rails, check out the following post.

Concerns in Rails: Everything You Need to Know
Concerns are an important concept in Rails that can be confusing to understand for those new to Rails as well as seasoned practitioners. This post explains why we need concerns, how they work, and how to use them to simplify your code.

Let's inspect the code snippet introduced near the beginning of this article.

<form action="/posts" accept-charset="UTF-8" method="post">

  <input type="hidden" name="authenticity_token" value="n4FHeWc4WBJLi5wU1bBmQH2lIJNKizNmxNgDj9VAD-9OBhVWVbJr1-YbQ2KuvS4T8BBOYhaRTOpgRzqCHoT-hA" autocomplete="off">

  <div class="mb-7">
    <input type="text" name="post[title]" id="post_title">
  </div>
</form>

When you use the form_with helper to generate a form, Rails automatically inserts the hidden authenticity_token in the form. It also stores this token as a random string in the session, to which an attacker does not have access.

Passing Custom Authenticity Tokens

If you want to pass a custom authenticity token, you can pass it using the :authenticity_token option. This is useful when you build forms to external resources.

<%= form_with(url: sessions_path, authenticity_token: 'random_token') do |form| %>
<% end %>

# Generates

<input type="hidden" name="authenticity_token" value="random_token" autocomplete="off">

If you don't want the token for some reason, pass false to the above option.

<%= form_with(url: sessions_path, authenticity_token: false) do |form| %>
<% end %>

How Rails Verifies Authenticity Tokens

When the application starts and ActionController::Base class loads, Rails calls the RequestForgeryProtection#protect_from_forgery method from an initializer, passing :exception as the forgery protection strategy. It means that Rails will throw an exception for unverified requests.

# actionpack/lib/action_controller/railtie.rb

initializer "action_controller.request_forgery_protection" do |app|
  ActiveSupport.on_load(:action_controller_base) do
    if app.config.action_controller.default_protect_from_forgery
      protect_from_forgery with: :exception
    end
  end
end

To learn more about how Rails initializers work, check out following post.

A Brief Introduction to Rails Initializers
After loading the framework and any gems in your application, Rails runs the initializers under the `config/initializers` directory. This article explains how initializers work and how they’re implemented.

The protect_from_forgery method adds a before_action callback on the controller, calling verify_authenticity_token method for each request.

# lib/action_controller/metal/request_forgery_protection.rb

def protect_from_forgery(options = {})
  self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
  self.request_forgery_protection_token ||= :authenticity_token
  before_action :verify_authenticity_token, options
end

Whenever a request arrives, Rails executes the verify_authenticity_token method, which verifies that the token in the request input matches the token stored in the session.

In its essence, this method checks the following conditions to ensure that the request is verified:

  1. Is it a GET or HEAD request? GET requests should be safe and idempotent.
  2. Does the authenticity token from the form match the stored token value?
def verified_request?
  !protect_against_forgery? || request.get? || request.head? ||
    (valid_request_origin? && any_authenticity_token_valid?)
end

When these conditions are satisfied, we know that the authenticated user is the one initiating the request.

Preventing Cross-Origin Requests

At this point, you might be wondering what's stopping an attacker from

  1. Write JavaScript code that makes a GET request to the Rails app,
  2. Parse its HTML contents to retrieve the authenticity token, and
  3. Use it to make a forged request by inserting the token as part of the request

The Rails app, upon receiving the request would be fooled into thinking it came from the user's app since it contains the authenticity token.

Rails prevents the above scenario by verifying if the request originated from the same origin by looking at the Origin header. The Origin request header indicates the origin (scheme, hostname, and port) that caused the request.

The same-origin policy is a critical security mechanism that restricts how a document or script loaded by one origin can interact with a resource from another origin.

Note the valid_request_origin? method above. Here's its implementation.

def valid_request_origin?
  if forgery_protection_origin_check
    # We accept blank origin headers because some user agents don't send it.
    raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
    request.origin.nil? || request.origin == request.base_url
  else
    true
  end
end

Typically, browsers add the Origin request header to:

Since the attacker's Origin header won't match the request's base_url, the valid_request_origin? method will return false, and Rails will handle the request just like an unverified request.

How to Disable CSRF Protection in Rails

Sometimes, you want to disable the CSRF mechanism on specific controllers, for example, a controller that handles POST callback requests from Stripe to process payments. As Stripe doesn't know your CSRF token, Rails will block these requests.

You can disable CSRF protection on a specific controller using the skip_forgery_protection method.

class StripeController < ApplicationController
  skip_forgery_protection
end

Behind the scenes, it calls skip_before_action to prevent calling verify_authenticity_token method.

def skip_forgery_protection(options = {})
  skip_before_action :verify_authenticity_token
end

This will allow any callback requests from Stripe to your application.

Conclusion

I hope you have a better understanding of how a Cross-Site Request Forgery attack works and how Rails mitigates it using authenticity tokens. Though it's not a common vulnerability, it's an important one to safeguard your application from.

Here's how Rails guides describe it:

CSRF appears very rarely in CVE (Common Vulnerabilities and Exposures) - less than 0.1% in 2006 - but it really is a 'sleeping giant' [Grossman]. This is in stark contrast to the results in many security contract works - CSRF is an important security issue.

If you are not using Rails form helpers, please use an authenticity_token to protect your POST, PUT/PATCH, and DELETE methods. Also, make sure that none of the GET requests cause any side effects on the server.


I hope you found this article useful and that you learned something new.

If you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.

Please subscribe to my blog if you'd like to receive future articles directly in your email. If you're already a subscriber, thank you.