This post is a follow-on to our CORS post back in December. We'll describe how traditional CORS policies aren't sufficient defense against cross-site request forgery (CSRF) attacks, and unveil a new Node module that layers CSRF protection on top of such policies, cors-gate. We'll show how an Origin-based approach has fewer moving parts than CSRF, and pairs neatly with CORS to protect your users against CSRF attacks. Note that this approach depends on modern browser functionality, and will not work if you're targetting older browsers.
Using the Origin and Referer headers to prevent CSRF
Cross-Site Request Forgery (CSRF) allows an attacker to make unauthorized requests on behalf of a user. This attack typically leverages persistent authentication tokens to make cross-site requests that appear to the server as user-initiated. Prior to our mitigation, a user visiting a third-party website while logged in to Mixmax could allow that website to make unauthenticated requests. Using CSRF, that website could execute actions with the user's Mixmax credentials.
Moreover, for preflighted requests, the browser will make an OPTIONS request prior to making the actual data-laden request, so the server won't have a chance to perform the unauthorized request. For simple requests, however, the browser makes the preflight request and simply disallows reading data from the response if the server does not give explicit permission. If the server isn't careful, though, it can still process the request.
Until recently, Mixmax was vulerable to cross-site request forgery in spite of our existing CORS implementation. CORS, after all, does not restrict access to data, but instead instructs the browser to specifically allow access to responses from cross-origin requests. As such, while it accidentally disables some cross-origin actions (by nature of the OPTIONS preflight), it does not block all requests. A user visiting a third-party site could have that site send an email on their behalf, log them out, or even log them into an attacker-controlled account. Even safe GET requests, which per the HTTP specification should not cause side-effects, can unnecessarily consume resources and may represent a denial-of-service (DOS) attack vector.
However, in all of these cases, the browser does send a Referer header. We can make use of the Referer header, which browser-initiated requests may not spoof, in place of the missing Origin header.
We have an additional constraint: because we identify the current user by the user query parameter like firstname.lastname@example.org, we have to make sure the Referer doesn't leak to third-party websites. We can prevent such leaks with the relatively new Referrer-Policy header. When sent by our services, this header governs the conditions under which the browser may expose referrer data to the same or other services within HTTP requests. We would ideally set a policy of strict-origin, as this gives us the requester's origin without any sensitive path information and without exposing the user's browsing history over an insecure connection.
Chrome does not support strict-origin as of 06.12.2017. Its implementation only allows no-referrer (and thus no origin, in cases where Origin is not available), no-referrer-when-downgrade (exposes path information), and origin-when-cross-origin, same-origin and unsafe-url, all of which leak referrer data over HTTP. We chose the best available option, no-referrer.
Thankfully, Firefox does support strict-origin. This lets us accomplish the crucial goal of preventing CSRF attacks while preserving permissible same- and cross-origin access. When Chrome and Safari add support for strict-origin, we can prevent unauthorized cross-origin access even to GET requests.
During our implementation, we came across a final quirk of Chrome's implementation: the Referrer-Policy has unintended consequences on form-submitted POST requests. As of Chrome 58, the no-referrer policy makes form POSTs send Origin: null for both same- and cross-origin POSTs. Safari sends the correct Origin header regardless of the presence of the meta referrer. With Referrer-Policy set to strict-origin, Firefox sends no Origin header, but does send the Referer.
To implement this defense, we published cors-gate, a module which halts request processing when the request does not definitively originate from a trusted domain. To best interoperate with our existing CORS middleware, cors-gate comes after the cors module, and reads the response headers to determine whether the request should be allowed per CORS. The module also checks the Origin against the server's current origin, as defined at startup. The middleware permits all safe requests (GET, HEAD) by default, as we cannot reliably determine their Origin, and they should have no side effects.
This snippet sets up an Express app to permit cross-origin requests from https://app.mixmax.com and https://other-app.mixmax.com to https://api.mixmax.com. Requests from other origins will fail outright, while requests from https://api.mixmax.com (same-origin requests) will continue to function.
const cors = require('cors');
const corsGate = require('cors-gate');
// if the Origin header is missing, infer it from the Referer header
// the expressjs/cors module
origin: ['https://app.mixmax.com', 'https://other-app.mixmax.com'],
// prevent cross-origin requests from domains not permitted by the preceeding cors rules
// require an Origin header, and reject request if missingstrict: true,
// permit GET and HEAD requests, even without an Origin headerallowSafe: true,
// the origin of the serverorigin: 'https://api.mixmax.com'
As a consequence of our chosen microservices architecture, clients and other microservices may make requests to the same endpoint. Moreover, we have an API gateway that enables third-party developers to build atop our platform, and we wish to allow cross-origin requests for third-party clients - provided they do not depend on the user being authenticated on our domains. To these ends, instead of immediately halting requests from unauthorized domains, we force require these requests to authenticate with an API-token, instead of with a cookie. Server-to-server and permitted third-party client-to-server requests can thus continue to operate, enabling their interaction with our services.
Alternatives for mitigating CSRF
We chose to implement a new module to mitigate CSRF attacks. Alternative solutions to CSRF protection also exist.
Use only JSON APIs (vs. e.g. application/x-www-form-urlencoded)
The unspoken assumption of this guideline is that a Content-Type header of application/json will trigger CORS preflighting, and if you haven't enabled CORS, the browser won't issue the actual request. This is all well and good except for that navigator.sendBeacondoesn't trigger preflight requests.
This solution is also undesirable for APIs where you do want to enable CORS, since preflighting will make your trusted requests slower.
Check the Referer header
This is almost equivalent to the proposed solution, but is incompatible with servers that set the Referrer-Policy header to protect their referrer data. By contrast, Referrer-Policy: no-referrer does not suppress the Origin header for AJAX requests.
GET should not have side effects
This is good advice regardless of CSRF, especially because no browser sends the Origin header with same-origin GET requests.
Like the "use only JSON" rule, these guidelines assume that requests will hit preflighting and fail. In this case, fair enough, since navigator.sendBeacon only uses POST. It is onerous to avoid using POST, though.
Don't support old browsers
Our proposed solution relies on this.
Depending on how CSRF tokens are implemented, they can indeed be extremely robust, and address the disadvantages of cors-gate by locking down same-origin GET requests.
We determined that this disadvantage is tolerable, so we prevent CSRF by using the simpler proposed solution. CSRF tokens introduce additional complexity for both clients and servers, and add non-negligible overhead due to their reliance on additional state.
Thanks to Jeff Wear for his diligent background research.
Thanks to Douglas Wilson for his work in maintaining Express' cors module and informing the development of cors-gate.
Interested in creating simple solutions to complex security problems? Join us!