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.

1class ApplicationController < ActionController::Base
2  protect_from_forgery with: :exception
3end

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.

1class ApplicationController < ActionController::Base
2  before_action :load_user
3  protect_from_forgery with: :exception
4end

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.

1class ApplicationController < ActionController::Base
2  before_action :load_user
3  protect_from_forgery with: :exception, prepend: true
4end

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.

1
2class ApplicationController < ActionController::Base
3  before_action :authenticate
4  protect_from_forgery unless: -> { @authenticated_by.oauth? }
5
6  private
7    def authenticate
8      if oauth_request?
9        # authenticate with oauth
10        @authenticated_by = 'oauth'.inquiry
11      else
12        # authenticate with cookies
13        @authenticated_by = 'cookie'.inquiry
14      end
15    end
16end
17

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.

1
2class ApplicationController < ActionController::Base
3  before_action :set_access_time
4  protect_from_forgery
5
6  private
7    def set_access_time
8      current_user.access_time = Time.now
9      current_user.save
10    end
11end
12

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.