The Life‑Changing Magic of Ruby and Rails

Understanding Authenticity Tokens in Rails

This post explores CSRF vulnerability and how Rails mitigates it using authenticity tokens. We will will learn why they're needed, how they're generated, how Rails uses them to verify the requests, and how to disable them for specific requests.

If you've been programming Rails for a while, you must have encountered a hidden input with a strange value in the rendered HTML forms. It looks something like this:

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

In this post, we will try to understand this hidden input called an authenticity token. We will learn why it's needed, how and where it's generated, how Rails uses this token to verify the requests, and how to disable it for specific requests.  

Cross-Site Request Forgery (CSRF)

Let's start with the why. Authenticity tokens are the solution to a security vulnerability known as Cross-Site Request Forgery or CSRF.

A CSRF attack tricks authenticated users into performing a dangerous activity on the application, such as transferring funds, changing the credentials, granting access to some protected resource, etc. Since the users are authenticated on the application, it doesn't prevent them from performing this activity.

CSRF Attack
CSRF Attack

The Open Web Application Security Project, or OWASP, describes this vulnerability as follows.

Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated. With a little help of social engineering (such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker’s choosing.

If the victim is a normal user, a successful CSRF attack can force the user to perform state-changing requests like transferring funds, changing their email address, etc. CSRF can compromise the entire web application if the victim is an administrative account.

How does a CSRF attack work?

Here's an example scenario that demonstrates how a CSRF attack works.

Step One: An attacker creates a fraud request that performs some dangerous activity.

Step Two: The attacker embeds this request into a hyperlink or a form and posts these links in many places on the internet, where unsuspecting users are likely to click them, such as forum comments, images, email links, etc.

Step Three: A victim is logged onto the application (e.g. banking website) in one tab and opens the email in the other. Then they click the fraud link in the email.

Step Four: Most web applications implement authentication using browser cookies. Cookies have nothing to do with tabs or windows -- they have to do with requests to a domain. Whenever the browser requests the web server for a domain, any cookies that the browser has for that domain will be sent in the request header. Since the user was logged into the application in the first tab, the browser sends the cookies from the request made from the second tab.

Step Five: Upon receiving the request, the web application checks the cookies and authenticates the user. Now it's thinking you're performing this request. However, this is the forged request created by the attacker. This request can now do anything that your application allows to logged-in users, such as transferring money from the bank account.  

Now, you might think that clicking a link or opening an image will make a GET request, which is harmless. Well, the attacker can easily write JavaScript code that creates and submits a form on the fly upon clicking a link or visiting a web page.

Here's an example that creates a form making a POST request to update your bank email and submits the form whenever the page loads.

<form action="https://your-bank.com/user/email" method="POST">
    <input type="email" value="malicious-email@example.com">
</form>
 
<script>
    document.forms[0].submit();
</script>

Now, all the hacker needs to do is trick you into visiting the page while you are logged into your bank website. Once the page loads, it will submit the form to the bank's website, passing the cookies. The bank website will think you are a genuine user and updates the email address.

How to Prevent CSRF Attack?

So the main problem behind the CSRF attack is that the server application is not differentiating between a genuine request (from the actual application) vs. the forged request that came from the attacker's code.

Only if there was a way for the server to identify the requests that came from the application, then it could reject all other requests that came from anywhere else.

We can solve this problem by instructing the server application to insert a unique, random token inside each form. Now, when the form is submitted from the application, this token is sent along with it. Upon receiving the request, the server checks if this token is present and matches the token it has. If it's present and matches, it knows that the request came from a genuine user and is valid. It rejects the request if the token is missing or doesn't match.

Since the token is random, the attacker can't guess and insert it into their forged requests. Hence the server rejects all forged requests, keeping the users safe.  

Preventing CSRF using Tokens
Preventing CSRF using Tokens

This is how Rails applications solve this problem, by using the authenticity_token to prevent CSRF attacks.

Rails Authenticity Tokens

Rails automatically generates a CSRF "token" whenever the application requests a form. Since this token is stored in the user's session and changes each time the session is regenerated, 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, only allowing the request if the two match. It raises an ActionController::InvalidAuthenticityToken error on unverified requests. 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.

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

Rails Inheritance Hierarchy
Rails Inheritance Hierarchy

How does Rails add authenticity tokens to the forms?

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.

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.

The form_with helper uses the token created by the form_authenticity_token method included by the RequestForgeryProtection concern above.

def form_authenticity_token(form_options: {})
  masked_authenticity_token(session, form_options: form_options)
end

How does Rails verify the authenticity tokens?

When the application starts and ActionController::Base class loads, Rails calls the protect_from_forgery method in the RequestForgeryProtection concern, passing the :exception as the forgery protection strategy.

# 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

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

Now, 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. When these two tokens match, we know that the authenticated user is the one initiating the request.

How to disable CSRF protection?

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 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

Conclusion

So 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 not idempotent methods (POST, PUT/PATCH, and DELETE). Also, make sure that none of the GET requests cause any side effects on the server.

If you have any questions or feedback, please send me an email. I look forward to hearing from you.

If you liked this post, and would like to receive future posts directly in email, please subscribe below. You might also enjoy:

Subscribe to Akshay's Blog

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe