This blog post will attempt to explain how Rails applications can protect themselves from Cross-Site Request Forgery (CSRF) by looking at the details of the built-in protection mechanisms.
Cross-Site Request Forgery is a serious vulnerability that stems from the trust that web applications place on the session identification cookies that are being passed between browser and server. For a more detailed explanation of CSRF, I suggest looking at the OWASP guide on Cross-Site Request Forgery.
Rails includes a built-in mechanism for preventing CSRF,
protect_from_forgery, which is included by default in the
application_controller.rb controller when generating new applications. This
protect_from_forgery method leverages magic to ensure that your application is protected from hackers!
Seriously though, many developers hardly recognize the threat imposed by CSRF, let alone the implementation details of the
So, first things first, just to be clear,
protect_from_forgery is not magic! There are implementation details which are important to understand. This is one of the cases where “the devil is in the details.”
Rails leverages synchronizer tokens (cryptographically random tokens) which are bound to the user's session. Within each form a hidden input field,
authenticity_token, is injected; this field contains the synchronizer token. The token is sent with the form submission request and is processed by the web application.
Upon processing the POST request (this is important -- only requests sent via POST will be verified), the server compares the value submitted for the
authenticity_token parameter to the value associated with the user’s session. If it doesn’t match, this indicates that the request may be a malicious request forged by an attacker. In this case, it is expected that the request will fail, protecting the application from CSRF.
Before we get too far, let me share a story about a recent assessment.
I was testing a Rails 3 application, and things appeared pretty secure. I ran Brakeman, a Rails static analysis tool, to discover potential vulnerabilities. There weren’t many findings, no mention of any CSRF vulnerabilities.
protect_from_forgery, as appropriate. All of the controllers inherited from
ApplicationController, and it appeared that there were no CSRF vulnerabilities. Through dynamic analysis, however, I discovered that the application was, in fact, vulnerable to CSRF.
I spent quite a while trying to track this down. The Rails logs showed the appropriate messages indicating that the attempt to match the authenticity token had failed, but the request was still being processed.
So this made me think: the request is obviously failing the validation, but why is the request executing successfully? To answer this question, I had to dig into the
ActionController source code (Note: the following code snippets are from ActionController 3.2.19).
When we call the
protect_from_forgery method, we're essentially creating a before filter, which calls the
verify_authenticity_token function runs, it checks if the request has been verified via a call to the
verified_request? method. Upon failing verification, it issues a warning (as we saw in the screenshots) and calls the
verified_request? method compares the
authenticity_token, which is stored in
session[:_csrf_token], with the POST parameters and the X-CSRF-Token HTTP header. If either the POST data or the HTTP header matches the session value, then the request is considered verified; otherwise, the method makes a call to
handle_unverified_request in turn calls the
ActionDispatch_reset_session helper which destroys the session associated with the forged request. The request continues being processed, now without an associated session. Seems to make sense, right? By removing the session information from the request, we strip any authentication details, and the request proceeds as if the user was never authenticated.
The complete (simplified) flow of the
protect_from_forgery method in Rails 3.2.19 can be seen below.
Upon further inspection, it appeared as if the application I was testing did not leverage Rails session helpers to manage sessions. Instead, this particular application used custom generated session identifiers which were stored in a cookie. Because this value was not stored in the session helper, it was not cleared within the
reset_session method, and as such, the requests succeeded despite failing the
Protecting the application
The application I was testing was vulnerable to CSRF because it leveraged cookies rather than Rails session helpers for handling authenticated sessions. So how can we protect the application?
By overloading the
handle_unverified_request in the application controller, we can redefine the behavior that will occur when a request fails verification. In this case, the application developers decided it would be appropriate to have the application throw an exception which would cause the request to halt execution. You can see an example below.
Things have changed!
Thankfully, things have changed a bit in Rails 4. Now, the
protect_from_forgery method accounts for cookies when purging the sensitive information from a forged request.
handle_unverified_request method has been restructured to call the
handle_unverified_request helper which removes session data, flash information, and associated cookies with each request that fails verification. But like in Rails 3, the request still executes. In some cases, this may be problematic.
Find the flaw
Consider the following example.
The application calls
protect_from_forgery, as appropriate. Because the funds transfer request receives its arguments directly from user input rather than the session, the method executes even if it fails the
verified_request? check. This means that an attacker can generate a forged request and trick a legitimate user into executing the request, transferring money from their bank account despite the call to
This behavior is a direct side effect of the implementation of the
handle_unverified_request method in Rails 4. Because this example relies on user parameters (insecure direct object reference) instead of cookie/session variables, the request generates
authenticity_token warnings but executes successfully, transferring the funds. This may be unexpected behavior and is definitely a security vulnerability.
Although it's never recommended to leverage direct object references without validation of ownership (leveraging account identifiers without verifying the user has access to the accounts), it's possible to protect similar functionality from CSRF by ensuring that an exception is raised when the application fails the
In Rails 4, we can pass an additional argument to the
protect_from_forgery method that causes it to raise an exception each time a request fails validation.
By raising an exception, we ensure that the request fails to execute and the funds will not be transferred.
CSRF is a serious vulnerability that is very widespread. In Rails, the
protect_from_forgery method helps protect applications from this vulnerability. Unfortunately, however, this method is not a catchall and, as we have seen, there are certain situations in which your application will still be vulnerable. Static analysis tools such as Brakeman will not be aware of the special circumstances outlined above and will not alert you of the potential vulnerabilities.
It is very important to understand the implementation details of security functions, such as
protect_from_forgery, to ensure that your application is not vulnerable to the specified threat.