CSRF in Rails
- It’s been there almost since the beginning, and it’s one of those features in Rails that makes your life easier without needing to give it a second thought.
- There are two components to CSRF. First, a unique token is embedded in your site's HTML. That same token is also stored in the session cookie. When a user makes a POST request, the CSRF token from the HTML gets sent with that request. Rails compares the token from the page with the token from the session cookie to ensure they match.
How is CSRF protection enabled?
- This has been in Rails for ages, and so we barely need to think about it. But how is this actually implemented under the hood?
- Generation and encryption - We'll start with #csrf_meta_tags. It's a simple view helper that embeds the authenticity token into the HTML:
# actionview/lib/action_view/helpers/csrf_helper.rb
def csrf_meta_tags
if defined?(protect_against_forgery?) && protect_against_forgery?
[
tag("meta", name: "csrf-param", content: request_forgery_protection_token),
tag("meta", name: "csrf-token", content: form_authenticity_token)
].join("\\n").html_safe
end
end
- The csrf-token tag is what we're going to focus on, since it's where all the magic happens. That tag helper calls #form_authenticity_token to grab the actual token. At this point, we've entered ActionController's RequestForgeryProtection module.
- The RequestForgeryProtection module handles everything to do with CSRF. It's most famous for the #protect_from_forgery method you see in your ApplicationController, which sets up some hooks to make sure that CSRF validation is triggered on each request, and how to respond if a request isn't verified. But it also takes care of generating, encrypting and decrypting the CSRF tokens. What I like about this module is its small scope; aside from some view helpers, you can see the whole implementation of CSRF protection right in this single file.
# actionpack/lib/action_controller/metal/request_forgery_protection.rb
# Sets the token value for the current session.
def form_authenticity_token(form_options: {})
masked_authenticity_token(session, form_options: form_options)
end
# Creates a masked version of the authenticity token that varies
# on each request. The masking is used to mitigate SSL attacks
# like BREACH.
def masked_authenticity_token(session, form_options: {}) # :doc:
# ...
raw_token = if per_form_csrf_tokens && action && method
# ...
else
real_csrf_token(session)
end
one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
masked_token = one_time_pad + encrypted_csrf_token
Base64.strict_encode64(masked_token)
end
- Let's continue diving into how the CSRF token ends up in your HTML. #form_authenticity_token is a simple wrapper method that passes any optional parameters, as well as the session itself, down into #masked_authenticity_token:
- Since the introduction of per-form CSRF tokens in Rails 5, the #masked_authenticity_token method has gotten a bit more complex. For the purposes of this exploration, we're going to focus on the original implementation, a single CSRF token per request - the one that ends up in the meta tag. In that case, we can just focus on the else branch of the conditional above, which ends up setting raw_token to the return value of #real_csrf_token.
# actionpack/lib/action_controller/metal/request_forgery_protection.rb
def real_csrf_token(session) # :doc:
session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
Base64.strict_decode64(session[:_csrf_token])
end
- Why do we pass session into #real_csrf_token? Because this method actually does two things: it generates the raw, unencrypted token, and it stuffs that token into the session cookie:
- Remember that this method is ultimately being called because we invoked #csrf_meta_tags in our application layout. This is classic Rails Magic - a clever side effect that guarantees the token in the session cookie will always match the token on the page, because rendering the token to the page can't happen without inserting that same token into the cookie.
- Anyway, let's take a look at the bottom of #masked_authenticity_token {go to previous slides}
one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
masked_token = one_time_pad + encrypted_csrf_token
Base64.strict_encode64(masked_token)
- Time for some cryptography. Having already inserted the token into the session cookie, this method now concerns itself with returning the token that will end up in plaintext HTML, and here we take some precautions (mainly to mitigate the possibility of an SSL BREACH attack, which I won't go into here). Note that we didn’t encrypt the token that goes into the session cookie, because as of Rails 4 the session cookie itself will be encrypted.
- First, we generate a one-time pad that we'll use to encrypt the raw token. A one-time pad is a cryptographic technique that uses a randomly-generated key to encrypt a plaintext message of the same length, and requires the key to be used to decrypt the message. It's called a "one-time" pad for a reason: each key is used for a single message, and then discarded. Rails implements this by generating a new one-time pad for every new CSRF token, then uses it to encrypt the plaintext token using the XOR bitwise operation. The one-time pad string is prepended to the encrypted string, then Base64-encoded to make the string ready for HTML.