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.
XMLHttpRequest cannot load https://app.mixmax.com/api/foo. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://evil.example.com' is therefore not allowed access.
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.
There are a variety of ways to defend against such attacks. The simplest is to check that the request originates from a trusted site, using the Origin request header. This is also the top solution recommended by the OWASP.
Origin header is the same header examined by the cors Node module when adding CORS response headers. However, such modules generally stop short of failing requests, as a matter of complying with the CORS specification and separating the concerns of allowing vs restricting access. Per cors' maintainer, "a CORS failure should look exactly as if the server has no idea what CORS is - in which case the request will still go through," (just without the CORS headers).
Another reason CORS modules avoid implementing CSRF protections is that browsers may not send the
Origin header, as in the following cases:
- img tags will not send Origin headers unless the crossorigin attribute is set
- Chrome and Safari will not send Origin headers with same-origin GET requests
- Firefox will not send the Origin header with any same-origin requests (bug)
- Firefox will not send the Origin header with cross-origin form POSTs (bug)
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
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
unsafe-url, all of which leak referrer data over HTTP. We chose the best available option,
Safari doesn't support
Referrer-Policy at all, but rather an older draft of the specification with
default values, the latter being equivalent to
no-referrer-when-downgrade. We again choose the best available option,
never, to avoid leaking referrer data over HTTP.
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 (
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://api.mixmax.com. Requests from other origins will fail outright, while requests from
https://api.mixmax.com (same-origin requests) will continue to function.
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.
To run down https://github.com/pillarjs/understanding-csrf as of 06.13.2017:
Use only JSON APIs (vs. e.g.
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.sendBeacon doesn'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.
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.
"Avoid using POST" and "don't use method-override!"
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!