Rails + Rack::Csrf
This post assumes you’re familiar with CSRF attacks:
Here, I will go over how to use Rack::Csrf
in a Rails app. That might leave a few people scratching their
heads… why not just use built-in CSRF protection?
As your app it evolves, you might find good use in having middleware intercept certain requests. In Rails, CSRF
protection happens at the controller layer, so it is completely possible that some of these requests will completely
bypass it. Examples are always good: say you have a Rack::Proxy
who’s job is to intercept requests going to
/external_api/*
and forward to an external web service. In cases like this, we need to set up a “catch-all system”—a
system where the authenticity of all non-GET requests is being verified as coming from a valid source, even those
that don’t hit the controller layer of the Rails app.
💢 Rack::Csrf
💢
This is just a small Rack middleware whose only goal is to lessen the hazards posed by CSRF attacks by trying to ensure that all requests of particular types come from the right client, not from a mischievous impersonator. (Source: Github)
CSRF Token Generation and Association with the Current Session
Since we’re going to use Rack::Csrf
, we can remove the line protect_from_forgery
from our application
controller, preventing forgery protection from happening at Rails’ controller layer. However, removing this line does
not necessarily prevent Rails from generating a CSRF token for the current user and associating it with the current
session; in fact, it’s possible for Rails or Rack::Csrf
to do this. As a consequence, we have to make
sure both Rails and Rack::Csrf
are reading/writing from/to the same place. In my case, it was enough to configure
Rack::Csrf
to use _csrf_token
(Rails’ default) as the key. In config.ru
:
use Rack::Csrf, key: '_csrf_token'
Token Verification for non-GET requests
Going back to our Rack::Proxy
example: how do you make sure all non-GET requests are being verified as coming from a
valid source? Make sure Rack::Csrf
comes after your session middleware—to verify the authenticity token—and before
Rack::Proxy
(i.e. before the request is intercepted).
With this configuration Rack::Csrf
will intercept all non-GET requests. When the authenticity token in the request
does not match the token associated with the current session, Rack::Csrf
throws a Rack::Csrf::InvalidCsrfToken
exception and the app 500s. (Alternatively, we can have the app 403 by removing the raise: true
option).