If you’ve worked with any number of Ruby on Rails projects you’ll know that people put a lot of effort into getting around Rails’ conventions. You’ll usually see this when the developers came from other frameworks, refuse to do things the Rails Way™, or when they were facing pressure to “just get things done.”
One of the work-arounds I’ve seen most frequently is bypassing CSRF protection altogether with the following line in the controller(s):
skip_before_action :verify_authenticity_token
This shouldn’t come as a surprise. The error messages can be cryptic, and the only place in the Rails Guides CSRF is mentioned is in the Ruby on Rails Security Guide and briefly in the csrf_meta_tags
page. You’d think it would be covered in Working with JavaScript in Rails, but there isn’t a single mention of it. Based on some of the “answers” I’ve seen in the past, you might even conclude that skipping the authenticity token was standard practice when handling JavaScript requests. Of course, that’s not the case.
CSRF (Cross-Site Request Forgery) is a method of attack that “works by including malicious code or a link in a page that accesses a web application the user is believed to have authenticated. If the session for that web application has not timed out, an attacker may execute unauthorized commands.” (Cross-Site Request Forgery (CSRF))
The idea is that because a user remains “signed in” even after leaving a site, a “black hat” site can make requests to the signed in site as if it was the user.
The Rails Guides has an excellent example scenario:
- Bob browses a message board and views a post from a hacker where there is a crafted HTML image element. The element references a command in Bob’s project management application, rather than an image file:
<img src="http://www.webapp.com/project/1/destroy">
- Bob’s session at
www.webapp.com
is still alive, because he didn’t log out a few minutes ago.- By viewing the post, the browser finds an image tag. It tries to load the suspected image from
www.webapp.com
. As explained before, it will also send along the cookie with the valid session ID.- The web application at
www.webapp.com
verifies the user information in the corresponding session hash and destroys the project with the ID 1. It then returns a result page which is an unexpected result for the browser, so it will not display the image.- Bob doesn’t notice the attack - but a few days later he finds out that project number one is gone.
Notice in the above scenario that the attack comes from a “GET” request in the img
tag. While we might laugh thinking no one would have a route like the one above, I have seen enough Rails projects to know that it’s common to see “GET” routes in place of what should be “POST”, “PUT”, or even “DELETE” actions.
The problem with using a “GET” requests for actions other than retrieving data is that Rails only checks CSRF tokens for non-“GET” requests.
Rails, like most modern frameworks, comes with CSRF protection baked in. It does so by setting the CSRF token in the session, in the meta
tags in your site’s <head />
area, and as a hidden field in each form.
Alex Taylor explains how this works:
There are two components to CSRF. First, a unique token is embedded in your site’s HTML. That same token is also stored in the session cookie. When a user makes a POST request, the CSRF token from the HTML gets sent with that request. Rails compares the token from the page with the token from the session cookie to ensure they match. — A Deep Dive into CSRF Protection in Rails
Without any intervention on your part, Rails sets the session cookie and — if you’re using form_for
or form_tag
— adds the hidden form field with the name, authenticity_token
. Even the “csrf-token” meta
tag is inserted for you as long as you leave the csrf_meta_tags
line in your application.html.erb
layout file.
When a request is made, Rails ensures both the session cookie and the form parameter match up. It does this with the protect_from_forgery
line in your ApplicationController
. All controllers subclassed from ApplicationController
inherit the same protection.
Let’s face it, you have to try pretty hard to get Rails not to use CSRF protection.
If you’re working with traditional forms which require a full page refresh after submission, there isn’t much more to talk about. Rails takes care of everything for you. If you’re using remote: true
in your forms, Rails again takes care of everything for you thanks to the jquery-rails
gem.
Many sites don’t use traditional forms anymore. They’re either juiced up with a little jQuery AJAX magic or are completely generated with a JavaScript framework like React.js or Vue.js. In these instances, the forms are either not submitted in toto or the forms are being generated by the frameworks themselves.
In these instances, PUTs, PATCHes, POSTs, and DELETEs need to send along the CSRF token. To do that, your code must first retrieve the token from the header. Next, it needs to send it along with the form submission.
As already discussed, Rails includes the CSRF token in the head
of your application.html.erb
layout file with the csrf_meta_tags
helper. All you need to do is retrieve it.
Retrieving the CSRF token with jQuery:
var token = $('meta[name=csrf-token]').attr('content');
Retrieving the CSRF token with JavaScript
var token = document.getElementsByName('csrf-token')[0].content
Once the token is retrieved, you need to include it in the headers when you send data to the server. Below are examples of how to do this with both jQuery and JavaScript’s fetch
.
Using jQuery’s ajax
method:
var token = $('meta[name=csrf-token]').attr('content');
var data = { post: { title: "some title", content: "Lorem ipsum dolor" } }
$.ajax({
url: '/api/v1/posts',
type: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token)
},
data: data,
success: function(response) {
doSomething(response);
}
});
Using JavaScript’s fetch
method:
var token = $('meta[name=csrf-token]').attr('content');
var data = { post: { title: "some title", content: "Lorem ipsum dolor" } }
fetch('/api/v1/posts', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
credentials: 'same-origin'
}).then(function(response) {
return response.json();
}).then(function(data) {
console.log(data);
});
By default, Rails applies CSRF protection to all controllers that subclass from ApplicationController
using the following line:
protect_from_forgery with: :exception
But are there instances where you’d want to respond differently, or even ignore CSRF protection altogether? The answer, of course, is “yes”.
If your project or a portion of your project uses an alternative method for authentication such as API tokens or any other “stateless” authentication, then you can safely remove the protect_from_forgery
line from whatever base class those controllers inherit.
On the other hand, if your project uses stateful authentication and APIs, such as those projects with lots of AJAX requests, it can be advantageous to use :null_session
with protect_from_forgery
like so:
protect_from_forgery with: :null_session
That means the user won’t be logged in anymore for that action and can’t perform the change (if the action requires a signed in user). However, after the action the session values will be back and the session ID will be the same, so the user will be logged in.
— Ruby on Rails Security Project
Rather than throwing an exception, which your JavaScript may not be able to handle, it instead sets the session value to nil
for the duration of the action. By doing this, any authorization or action scoped to the current user will result in errors which your JavaScript can more easily work with.
In the “bad old days” of web development, we didn’t concern ourselves with security features such as CSRF—or even password hashing, but let’s not talk about that. It wasn’t out of laziness or even carelessness that we didn’t; it just wasn’t common knowledge. Unless you knew what to look for, you usually didn’t know what to look for.
Thankfully, today’s frameworks come with many of these “features” built in. While they are there to simplify security and protect us from our ignorance, they can be difficult to navigate around. By working with the framework and taking the time to understand why these “roadblocks” are put in place, we protect ourselves and only make our work better.