Cross-site request forgery or CSRF is a well known attack that has been vastly documented.
To deal with this, Rails has the RequestForgeryProtection
module that gives access to protect_from_forgery
. It’s now set by default when you create a new Rails project and takes the form of a single line of code in the application controller:
This with
parameter is actually the forgery_protection_strategy
parameter, it tells Rails how to behave when a CSRF attack is identified.
The Different Forgery Protection Strategies
There are 3 strategies currently built into the RequestForgeryProtection ::ProtectionMethods
module of ActionController
: exception
, null_session
and reset_session
.
The null_session
strategy is the default one. The gotcha here is that by default Rails 5 generates the ApplicationController
file with the exception
strategy, but the default strategy inside ActionPack is actually the null_session
one, which can be confusing.
Exception
This is the one Rails 5 sets up by default. It will raise an exception if a CSRF attack occurs:
This strategy ensures that the execution stops right after the verify_authenticity_token
check if the request is fraudulent.
Null Session
This strategy will not cause the app to crash but will instead nullify the session for the duration of the request.
Note that while this is now the default, Rails 3 didn’t generate the ApplicationController
file with the with: :exception
parameter, so you didn’t touch a thing and have an old app that you kept on updating, you might have the null_session
strategy set up and not even know it.
Reset Session
The third strategy, reset_session
, simply calls the reset_session
of @controller
as you can see here.
Important Note On Security
It is very important to keep in mind that fraudulent requests will go through with the null_session
and reset_session
strategies. The only action taken by these strategies is to make sure the session is empty during the request.
As an illustration here are the logs for a fraudulent request:
Started POST "/posts"
Processing by PostsController#create as */*
Can't verify CSRF token authenticity.
Completed 200 OK in 2ms (Views: 1.0ms | ActiveRecord: 0.0ms)
I think that it is quite counter intuitive and might cause serious security concerns depending on your implementation. Brakeman even considers using other strategies to be an issue.
If you want to learn more about this, Jason Yeo explains it in depth in his great article, “When Rails’ protect from forgery Fails”. If you’re unsure of your implementation, the safe approach is to use the exception
strategy.
Building A Custom Strategy
In some cases it can be interesting to build your own strategy. Let’s say you don’t want to return 500s when a CSRF attack occurs, but you still want to be warned or at least have better logs.
How It Works
First, let’s take a look at how all this works:
The requests goes through verify_authenticity_token
. If there is an issue, it then logs a warning and then calls the handle_unverified_request
method of the forgery_protection_strategy
.
Getting An Exception
First, let’s get a InvalidAuthenticityToken
exception. If you want to do it properly, you probably want to do this writing tests, but to demonstrate this I’ll be using curl.
Start your server and call a POST action that has protect_from_forgery
activated using curl. This way you’re sure that you don’t have the proper authenticity token.
$ curl -X POST -I http://localhost:3000/secure_post_action
You should see this in your logs:
Can't verify CSRF token authenticity.
Completed 422 Unprocessable Entity in 1ms (ActiveRecord: 0.0ms)
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken)
Adding A Custom Forgery Protection Strategy
Now that we know how to quickly test, let’s add our new strategy.
Any forgery propection strategy needs to be initalized with a controller and respond to handle_unverified_request
. So the bare minimum here is:
Then you can use it by changing your controller code:
Now you can get your strategy to do whatever you’d like and check the result by running the curl command. To give you ideas, here’s what I implemented on a project to get a bit more logs while still falling back on the null_session
strategy:
Since you scrolled this far, you might be interested in some other things I wrote:
- Add Text Over an Image with Ruby
- Misuse of update/update! in Ruby on Rails
- Regression Testing For Data
- Expressing Intent Without Comments In Ruby
- The Difference Between to_s & to_str In Ruby
- First Impressions: Rails 5 on Google App Engine
- Automatically Run RSpec on Multiple Projects
- Startup & Tech Book Reviews