I have an domain, api.example.com, that handles all API requests for my service. There are a select set of domains, [ www.example.com, *.www.example.com ], from which I wish to allow browser access. In addition to this, authorized clients should be able to access the API with non-browser clients (e.g. cURL). I have enabled CORS protections as best I can to allow www.example.com and *.www.example.com to make cross-origin requests to api.example.com.
I wish to enable as much Cross-Site-Forgery-Request protection as possible on api.example.com while still allowing access from protected domains.
The traditional solution to CSRF protection is the Double-Submit Cookie. This solution works when the host page and any AJAX requests are made against the same domain. The server issues a separate csrf-token cookie valid only for that domain that the client then extracts and includes as a header with every AJAX request.
Unfortunately, setting a cookie on api.example.com would not allow the browser access to it, so it would not be able to set the header.
We set the cookie on example.com so the browser has access. This is a relatively simple solution. Unfortunately, it greatly expands the risk profile of the service since an XSS attack on any *.example.com page would break the security model.
http://stackoverflow.com/questions/24680302/csrf-protection-with-cors-origin-header-vs-csrf-token suggests that, when combined with good CORS headers, simply checking the Origin header on requests to api.example.com should be sufficient. I'm not convinced that's true, but I'm also not convinced it's false. One question is when should I check the Origin header? Non-browser API clients wouldn't send it. Should I only validate it if it's present, under the assumption that all modern browsers will set it correctly?
- We create an HTML page,
api.example.com/share-crsf-token. The response to that page -- and only that page -- includes the CSRF token as a cookie (onapi.example.com). - We set
Content-Security-Policy: frame-ancestors www.example.com *.www.example.comto prevent sites other than those on our intended list from embedding this page in an iframe. (We can't use the deprecatedX-Frame-Optionsbecause Chrome doesn't support theAllow-Fromoption.) Older IE versions useX-Content-Security-Policyinstead, so we set that to the same. - JavaScript on that page checks to make sure
document.referrermatches/^https://(.*\.)?www\.example\.com$/. If not, it aborts. - JavaScript in the page extracts the cookie and calls
window.parent.postMessage('csrf-token:jfdai13m402vaje', document.referrer)to announce the token to the parent window. - JavaScript on
www.example.comand*.www.example.comlisten formessageevents, ensure that the sender ishttps://api.example.com, and then save the token in localstorage.
Risks:
- not all browsers obey
Content-Security-Policy - we're relying on the single-origin policy as applied to cookies and localstorage, as well as the security model of
postMessage; that's a bigger surface than just cookies
On a successful POST https://api.example.com, we generate a token, jfdai13m402vaje, and redirect to https://www.example.com/set-csrf-token/jfdai13m402vaje (or a subdomain thereof, based on Referer). The resource www.example.com/set-csrf-token/[token] simply returns a cookie with the provided token.
The problem here is that a malicious third party could easily commit a minor denial-of-service attack against our users by forcing them to go to https://www.example.com/set-csrf-token/some-wrong-token. It wouldn't compromise confidentiality or integrity (since the attacker can't generate a cryptographically secure token), but it would effectively log the user out.
CSRF and CORS don't play together very nicely. These are just the ideas I've come up with. I'm hoping someone else has some better ones... or a good reason to pick one of these.
Some more ideas I've heard:
OAuth2
Implement an OAuth2 service provider system on the back-end. Use a short refresh period to gain the security that the double-submit cookie provides.
I think that's probably the best long-term solution I've heard. It does require building out a new authentication service, which can be expensive and slow to develop and deploy.
Single Domain, Proxy
Give up on using
api.example.comdirectly from the browser. Instead, send them to the page's domain and use a path prefix to disambiguate API requests --GET /api/posts/437.You'll need some sort of proxy service like Varnish to forward the API requests.
The problem with this is
links in the API responses. If using JSON API, your JSON response might look like{ "links": { "self": "https://api.example.com/posts/437" }, "data": [{ "type": "posts", "id": "1",The proxy will have to rewrite all those URLs -- something that's both error-prone and slow.
You could mitigate that by having the API server use the
X-Forwarded-ForURL as the base.