CSRF and RAILS protect_from_forgery

Cross-site request forgery, also known as a one-click attack or session riding and abbreviated as CSRF or XSRF, is a type of malicious exploit of a website whereby unauthorized commands are transmitted from a user that the website trusts. Unlike Cross Site Scripting (XSS), which exploits the trust a user has for a particular site, CSRF exploits the trust that a site has in a user’s browser.Lets take a look at the schematic of the CSRF

CSRF Scheme
  • Step1: The Victim connect to secure Bank websites and logs into his account.
  • Step2: A cookie set in the Victims browser containing the session id of the victim.
  • Step3: Victim trips into visiting a malacious page.
    Step4: Victim recieves a html page containig malacious hidden form.
  • Step5: A web request is executed from the victims browser carrying the context of cookie set in Step2.
  • Step6: Bank Server completes the web requests.

Conclusion: Banking server failed to verify the validity of the web request and hence executed it without the victims knowledge.

Now we know what CSRF is, lets see how Rails help prevent CSRF.
As Rails uses MVC architecture, Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks by including a token in the rendered html for your application. This token is stored as a random string in the session, to which an attacker does not have access. When a request reaches your application, Rails verifies the received token with the token in the session. Only HTML and JavaScript requests are checked, so this will not protect your XML API (presumably you’ll have a different authentication scheme there anyway). Also, GET requests are not protected as these should be idempotent. The requests are validated using the following peice of code

def verified_request?
!protect_against_forgery? || request.get? || request.head? ||
form_authenticity_token == params[request_forgery_protection_token] ||
form_authenticity_token == request.headers['X-CSRF-Token']
end

This can be enabled with the protect_from_forgery method, which will perform the check and handle unverified requests, if the token doesn’t match. And it will add a _authenticity_token parameter to all forms that are automatically generated by Rails. It is recommended that this method is added in your ApplicationController, and later on you can skip it in other controllers if not required.

With all this in mind lets take a look at Rails source code.

class ApplicationController < ActionController::Base
protect_from_forgery
end
def protect_from_forgery(options = {})
self.request_forgery_protection_token ||= :authenticity_token
prepend_before_action :verify_authenticity_token, options
end
def verify_authenticity_token
unless verified_request?
logger.warn "Can't verify CSRF token authenticity" if logger
handle_unverified_request
end
end
def handle_unverified_request
reset_session
end

From the code, we figure out, CSRF protection resets session and lets the request through when CSRF token verification fails.
This in itself is a CSRF vulnerability since it allows anyone to logout users by directing their browser to a page that requires CSRF protection

With Rails 4 application, the ApplicationController now passes a parameter to protect_from_forgery.

class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
end
def protect_from_forgery(options = {})
self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
self.request_forgery_protection_token ||= :authenticity_token
prepend_before_action :verify_authenticity_token, options
end
def verify_authenticity_token
unless verified_request?
logger.warn "Can't verify CSRF token authenticity" if logger
handle_unverified_request
end
end
def handle_unverified_request
forgery_protection_strategy.new(self).handle_unverified_request
end

This raises an exception when an unverified request is encountered. Same behavior can be achieved with Rails 3 by overriding the default handle_unverified_request method.