Rails API design without disabling CSRF protection
Asked Answered
U

2

52

Back in February 2011, Rails was changed to require the CSRF token for all non-GET requests, even those for an API endpoint. I understand the explanation for why this is an important change for browser requests, but that blog post does not offer any advice for how an API should handle the change.

I am not interested in disabling CSRF protection for certain actions.

How are APIs supposed to deal with this change? Is the expectation that an API client makes a GET request to the API to get a CSRF token, then includes that token in every request during that session?

It appears that the token does not change from one POST to another. Is it safe to assume that the token will not change for the duration of the session?

I don't relish the extra error handling when the session expires, but I suppose it is better than having to GET a token before every POST/PUT/DELETE request.

Upanishad answered 29/9, 2011 at 16:21 Comment(0)
W
54

Old question but security is important enough that I feel it deserves a complete answer. As discussed in this question there are still some risk of CSRF even with APIs. Yes browsers are supposed to guard against this by default, but as you don't have complete control of the browser and plugins the user has installed, it's should still be considered a best practice to protect against CSRF in your API.

The way I've seen it done sometimes is to parse the CSRF meta tag from the HTML page itself. I don't really like this though as it doesn't fit well with the way a lot of single page + API apps work today and I feel the CSRF token should be sent in every request regardless of whether it's HTML, JSON or XML.

So I'd suggest instead passing a CSRF token as a cookie or header value via an after filter for all requests. The API can simply re-submit that back as a header value of X-CSRF-Token which Rails already checks.

This is how I did it with AngularJS:

  # In my ApplicationController
  after_filter :set_csrf_cookie

  def set_csrf_cookie
    if protect_against_forgery?
      cookies['XSRF-TOKEN'] = form_authenticity_token
    end
  end

AngularJS automatically looks for a cookie named XSRF-TOKEN but feel free to name it anything you want for your purposes. Then when you submit a POST/PUT/DELETE you should to set the header property X-CSRF-Token which Rails automatically looks for.

Unfortunately, AngualrJS already sends back the XSRF-TOKEN cookie in a header value of X-XSRF-TOKEN. It's easy to override Rails' default behaviour to accomodate this in ApplicationController like this:

  protected

  def verified_request?
    super || form_authenticity_token == request.headers['X-XSRF-TOKEN']
  end

For Rails 4.2 there is a built in helper now for validating CSRF that should be used.

  protected

  def verified_request?
    super || valid_authenticity_token?(session, request.headers['X-XSRF-TOKEN'])
  end

I hope that's helpful.

EDIT: In a discussion on this for a Rails pull-request I submitted it came out that passing the CSRF token through the API for login is a particularly bad practice (e.g., someone could create third-party login for your site that uses user credentials instead of tokens). So cavet emptor. It's up to you to decide how concerned you are about that for your application. In this case you could still use the above approach but only send back the CSRF cookie to a browser that already has an authenticated session and not for every request. This will prevent submitting a valid login without using the CSRF meta tag.

Wroth answered 24/2, 2013 at 20:46 Comment(8)
Your edit is crucial: The server should ONLY set the CSRF cookie for authenticated users.Kramatorsk
What would you suggest for auth then? Just plain forms based auth with the CSRF token set in the HTML as usual, and then API with header or cookie based CSRF after?Wroth
Setting a cookie without httponly doesn't seem safe. Wouldn't httponly prevent JS from accessing the CSRF cookie? Instead they would be passed along with the request if you use fetch's credentials : 'same-origin' parameter for example. And why use a non-conventional name for the cookie? If you use X-CSRF-Token ActionDispatch will check for it automatically with no override of verified_request? needed. I'd recommend: cookies["X-CSRF-Token"] = { value: form_authenticity_token, httponly: true }Intrigue
HttpOnly will not work, as your JS framework does need to access the cookie in order to return it as a header value. You can however just use the meta tag support. Also when I wrote this ActionDispatch did not automatically check for X-CSRF-Token.Wroth
@ChrisNicola and it still doesn't today.Cymose
@ChrisNicola, based on your answer I'm wondering here: #50160347 if we need to set a cookie after every action...Cymose
Yes I believe the token changes after every API call.Wroth
Is anybody running into an issue where the tokens are always httponly in the browser, even if they are explicitly set to httponly: false in the rails filter? I'm having trouble getting my frontend (in React) to be able to access the cookies.Summation
O
7

Rails works with the 'secure by default' convention. Cross-Site or Cross-Session Request Forgery requires a user to have a browser and another trusted website. This is not relevant for APIs, since they don't run in the browser and don't maintain any session. Therefore, you should disable CSRF for APIs.

Of course, you should protect your API by requiring HTTP Authentication or a custom implemented API token or OAuth solution.

Onset answered 30/9, 2011 at 12:13 Comment(3)
The page I linked disagrees: "Certain combinations of browser plugins and HTTP redirects can be used to trick the user’s browser into making cross-domain requests which include arbitrary HTTP headers specified by the attacker. An attacker can utilise this to spoof ajax and API requests and bypass the built in CSRF protection and successfully attack an application."Upanishad
"This is not relevant for APIs, since they don't run in the browser": sometimes the browser is the client of the API -- that is,Kasher
#9363410Thill

© 2022 - 2024 — McMap. All rights reserved.