Rails 5 changes protect_from_forgery execution order

Vijay Kumar Agrawal

Vijay Kumar Agrawal

April 6, 2016

This blog is part of our  Rails 5 series.

What makes Rails a great framework to work with is its sane conventions over configuration. Rails community is always striving to keep these conventions relevant over time. In this blog, we will see why and what changed in execution order of protect_from_forgery.

protect_from_forgery protects applications against CSRF. Follow that link to read up more about CSRF.

What

If we generate a brand new Rails application in Rails 4.x then application_controller will look like this.

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

Looking it at the code it does not look like protect_from_forgery is a before_action call but in reality that's what it is. Since protect_from_forgery is a before_action call it should follow the order of how other before_action are executed. But this one is special in the sense that protect_from_forgery is executed first in the series of before_action no matter where protect_from_forgery is mentioned. Let's see an example.

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

In the above case even though protect_from_forgery call is made after load_user, the protection execution happens first. And we can't do anything about it. We can't pass any option to stop Rails from doing this.

Rails 5 changes this behavior by introducing a boolean option called prepend. Default value of this option is false. What it means is, now protect_from_forgery gets executed in order of call. Of course, this can be overridden by passing prepend: true as shown below and now protection call will happen first just like Rails 4.x.

class ApplicationController < ActionController::Base
  before_action :load_user
  protect_from_forgery with: :exception, prepend: true
end

Why

There isn't any real advantage in forcing protect_from_forgery to be the first filter in the chain of filters to be executed. On the flip side, there are cases where output of other before_action should decide the execution of protect_from_forgery. Let's see an example.


class ApplicationController < ActionController::Base
  before_action :authenticate
  protect_from_forgery unless: -> { @authenticated_by.oauth? }

  private
    def authenticate
      if oauth_request?
        # authenticate with oauth
        @authenticated_by = 'oauth'.inquiry
      else
        # authenticate with cookies
        @authenticated_by = 'cookie'.inquiry
      end
    end
end

Above code would fail in Rails 4.x, as protect_from_forgery, though called after :authenticate, actually gets executed before it. Due to which we would not have @authenticated_by set properly.

Whereas in Rails 5, protect_from_forgery gets executed after :authenticate and gets skipped if authentication is oauth.

Upgrading to Rails 5

Let's take an example to understand how this change might affect the upgrade of applications from Rails 4 to Rails 5.


class ApplicationController < ActionController::Base
  before_action :set_access_time
  protect_from_forgery

  private
    def set_access_time
      current_user.access_time = Time.now
      current_user.save
    end
end

In Rails 4.x, set_access_time is not executed for bad requests. But it gets executed in Rails 5 because protect_from_forgery is called after set_access_time.

Saving data (current_user.save) in before_action is anyways a big enough violation of the best practices, but now those persistences would leave us vulnerable to CSRF if they are called before protect_from_forgery is called.

If this blog was helpful, check out our full blog archive.

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.