32
votes

I've been implementing a Rails 4 application with an API. I want to be able to call the API from mobile phones and the webapp itself. I came across this note while researching protect_from_forgery:

It's important to remember that XML or JSON requests are also affected and if you're building an API you'll need something like:

class ApplicationController < ActionController::Base
  protect_from_forgery
  skip_before_action :verify_authenticity_token, if: :json_request?

  protected

  def json_request?
    request.format.json?
  end
end

I was thinking of doing this, but I have some reservations/questions:

  1. This solution seems to leave the CSRF hole open because now an attacker could craft a link with an onclick javascript that posts JSON?
  2. Would checking for an API token be a reasonable substitute? i.e., what if instead of skipping the authenticity check, allowing it to fail the check and recover in handle_unverified_request if the api token is present and correct for current user?
  3. Or maybe I should just make the webapp and mobile devices send the CSRF token in the HTTP headers? Is that safe? How would the mobile phone even obtain the CSRF token, given that it isn't rendering HTML forms to begin with?

Edit for clarification:

I am more concerned about the webapp user clicking a crafted CSRF link. The mobile users are authenticated, authorized, an have an API key, so I am not concerned about them. But by enabling CSRF protection for the webapp users, the mobile users are blocked from using the protected API. I want to know the correct strategy for handling this, and I don't believe the Rails documentation gives the right answer.

2
You have two different scenarios: web apps and non-web apps. CSRF only works within web apps as the attack is dependent on leveraging an existing, valid, browser session. Therefore, if you're authenticating using browser sessions you'll need to use CSRF token protection. If you're authenticating using an API key then you can't suffer from CSRF as there's no valid session available to forge. If you use both techniques on the same API you'll need to use the appropriate method for each request type.toxaq

2 Answers

8
votes

An attacker could CURL at your controllers all they like, but if your API requires authentication, they wont get anywhere.

Making the API consumers send a CSRF is not really what CSRF does. To do this you'd need to implement a type of knocking mechanism where your client hits an authorization endpoint first to get the code (aka CSRF) and then submit it in the POST. this sucks for mobile clients because it uses their bandwidth, power, and is laggy.

And anyway, is it actually forgery (i.e. the F in CSRF) if its an authorized client hitting your controller after all?

6
votes

Sending the CSRF token in an HTTP header is indeed a common approach. It ensures that the client has somehow obtained a valid token. For example, a crafted CSRF link will be sent with credential cookies but the header will not include the CSRF token. Your own javascript on the client will have access to domain cookies and will be able to copy the token from a cookie to the header on all XHR requests.

AngularJS follows this approach, as explained here.

As for your first two questions:

  1. This solution seems to leave the CSRF hole open...

Indeed, which is why you should not disable the CSRF token also in your API.

  1. Would checking for an API token be a reasonable substitute? ...

Probably not. Take into consideration the following (from OWASP):

CSRF tokens in GET requests are potentially leaked at several locations: browser history, HTTP log files, network appliances that make a point to log the first line of an HTTP request, and Referer headers if the protected site links to an external site.

General recommendation: Don't try to invent the wheel. OWASP has a page called REST Security Cheat Sheet as well as the one I linked to before. You can follow the Angular approach (copying the token from a cookie to a header on each XHR request) and for regular non-ajax forms, be sure to use only POST and a hidden field as is normally done in CSRF protection of static server forms.