blogstatusbookshire me

Understanding Rails' Forgery Protection Strategies

22 August 2016

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:

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
end

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:

protect_from_forgery with: :exception

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.

protect_from_forgery
# or
protect_from_forgery with: :null_session

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.

protect_from_forgery with: :reset_session

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:

Google analytics drop

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:

class MyStrategy
  def initialize(controller)
  end

  def handle_unverified_request
  end
end

Then you can use it by changing your controller code:

class ApplicationController < ActionController::Base
 protect_from_forgery with: MyStrategy
end

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:

class LoggingForgeryStrategy
  def initialize(controller)
    @controller = controller
  end

  def handle_unverified_request
    Rails.logger.warn [
      "handle_unverified_request",
      "#{@controller.controller_name}##{@controller.action_name}",
      "params=#{request.params}"
    ].join(" - ")

    null_session.handle_unverified_request
  end

  private

  def request
    @request ||= @controller.request
  end

  def null_session
   ActionController::RequestForgeryProtection::ProtectionMethods::NullSession.new(@controller)
  end
end