Cross-Site Request Forgery and Rails

The problem

How Cross-Site Request Forgery works in Rails

What Rails does against it

Rails adds a token to every request that will be verified on the server. The server stores the token in the session, so it’s different for every user and different for every new session. Other websites cannot read the token in the session (by decoding the session cookie of your application) because they don’t have access to other website’s cookies. Tokens that were valid in the past for a certain user will not be valid anymore if the user signed out in between, or if the server expired the session. The purpose of the token is that an attacker doesn’t know the victim’s token and thus a CSRF attack without that token would be refused.

Isn’t it enough to make everything non-GET in my app?

You may have seen CSRF examples that use <img src="http://example.com/projects/destroy“ /> as an attack vector. The browser uses HTTP GET to fetch images, so isn’t it enough to convert this destroy action to HTTP POST (or DELETE)? No, attack websites can also use JavaScript to dynamically create (POST) forms and automatically submit that form. That’s why we need that token.

How does the token look like?

The token will be added automatically to every form like this: <input name="authenticity_token" type="hidden" value="OXuQV+9Q1hi5YkeynLQgVddCRfdUwl0huvqSjoqf4mE=" />.

Remote forms and the token

For remote forms, there will be the same token in a meta tag if <%= csrf_meta_tag %> is added in the layout view. If the token is still not there check application.js for //= require jquery_ujs for the unobtrusive JQuery plugin.
The unobtrusive JQuery script will add that token automatically to Ajax requests as an HTTP header called “X-CSRF-Token” or a parameter called authenticity_token (if the form is dynamically created). If for some reason you can’t use that script, look at this answer.

Configuration

The CSRF protection can be turned on with the protect_from_forgery controller method  and is included in the ApplicationController by default. So for every non-GET (and non-HEAD) action Rails will check the authenticity token. The first step is to check all routes. Everything that changes the state of the application should be non-GET so that the application will change state only if there’s an authenticity token. HTTP GET is for everything that is more like a question and POST/PUT/PATCH/DELETE to change the state of the application.
If some GET requests still need to change the state of the application (e.g. counting requests) or you’re thinking about rate limiting GET requests, refer to the Rack::Attack article.

JavaScript and CSRF

One exception to the above rule is that only Ajax requests are allowed to make GET requests for JavaScript responses. That’s due to an exception to the Same Origin policy of browsers that allows including <script> tags from different origins. So a site from a different origin could include a <script> tag that loads an authorized action via HTTP GET, runs it and then maybe extract secret information. An error will be thrown if someone tries that.

Different formats

The Rails documentation states that only HTML and JavaScript requests are checked. This might be misleading as also JSON requests in the main application will be checked by default. So before you skip_before_action :verify_authenticity_token in the application make sure that those actions don’t use the current user’s session which would make those actions vulnerable.

CSRF in the API?

If you want to skip the CSRF check for your API (as described in the Rails doc), you’ll have to make sure that your API does not work with the same authentication. Because if so, and there’s no CSRF protection in the API, an attacker can simply resort to the API URL to do the CSRF attack.
To check that, sign in to the application and then enter a URL of the API in the browser and see if you get an answer. If that works, you’ll want to test the Cross-Site Request Forgery behavior of the API. For that, choose a create action in the API, e.g. example.com/api/users. Now use a request or REST tool in your browser, like Request Maker in Chrome or RESTClient in Firefox. Try to create a new user with the API via example.com/api/users.json?user[login]=test. If this creates a user, this application is vulnerable to CSRF because you didn’t send the authenticity token that is required for non-GET requests in the rest of the application. You should use a different authentication scheme in the API. Popular approaches for authentication in the API are OAuth2 and API keys.

What happens if the token is missing or wrong?

In Rails 4 there are three „escalation strategies“: Throw an exception, create a new session or clear the current session.
  •  protect_from_forgery with: :null_session Set all values to nil in all cookies, including the 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. That’s the difference to the following one.
  • protect_from_forgery with: :reset_session Create a new session with a new session ID and no values in. That means the user won’t be logged in anymore. If you’re using cookie store for the session, then the old session still exists in the former cookie value (if the user copied it), but will be overwritten in the browser by the new cookie (with the new, empty session in) when the request comes back. Note that Devise by default won’t reset the „remember me“ cookie. That means if you use that feature and that cookie is present, Devise will sign me in with the new session and perform the change! Test that in your application and possibly overwrite the handle_unverified_request method.
  • protect_from_forgery with: :exception Raises an ActionController::InvalidAuthenticityToken exception which you can rescue and then return a header-only response or something more user-friendly:
rescue_from ActionController::InvalidAuthenticityToken do
  head :bad_request
end
 Note that this doesn’t sign the user out.

Caching

The form_tag() helper has an authenticity_token option to set a custom token (probably hardly necessary), or to completely remove it (probably also hardly necessary). But if you’re fragment caching a remote form, you won’t want the token to be cached. The config option config.action_view.embed_authenticity_token_in_remote_forms is false by default anyway because remote forms get the token from the meta tag by JavaScript, so it’s not included in remote forms. Set it to true if you need to support browsers without JavaScript, but then don’t cache the part with the token.

Can’t an external website just GET the token and then POST something?

So if the authenticity token is in a form, can another website use Ajax to retrieve that HTML (using the user’s session), parse the token and then send the CSRF attack, but include the token? That’s not possible because of the Same-Origin policy of browsers, an external website cannot send an Ajax request to another domain (unless explicitly allowed by our application).
Slight modification: Can the web server of that external web site GET the form from our application in the background, parse the HTML and then render a page with a CSRF attack and an authenticity token? No, because the entire idea of the vulnerability is that the attacker uses the victim’s user session in cookies in the browser – an external web server doesn’t have access to it.