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.