Using CORS policies to implement CSRF protection
A modern alternative to CSRF tokens
Tuesday, Jun 13th, 2017
Mixmax is a communications platform that brings professional communication & email into the 21st century.
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.
We previously discussed using CORS to secure user data, while allowing some cross-origin access. CORS handles this vulnerability well, and disallows the retrieval and inspection of data from another Origin. Without the cooperation of Mixmax servers, CORS will prevent the third-party JavaScript from reading data out of the image, and will fail AJAX requests with a security error:
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.
The 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 Origin
header.
We have an additional constraint: because we identify the current user by the user
query parameter
like user=foo@example.com
, 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
.
Safari doesn't support Referrer-Policy
at all, but rather an older
draft of the specification with never
, always
, origin
, and 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.
cors-gate
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
app.use(corsGate.originFallbackToReferrer());
// the expressjs/cors module
app.use(cors({
origin: ['https://app.mixmax.com', 'https://other-app.mixmax.com'],
credentials: true
}));
// prevent cross-origin requests from domains not permitted by the preceeding cors rules
app.use(corsGate({
// require an Origin header, and reject request if missing
strict: true,
// permit GET and HEAD requests, even without an Origin header
allowSafe: true,
// the origin of the server
origin: '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.
To run down https://github.com/pillarjs/understanding-csrf as of 06.13.2017:
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.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.
Disable CORS
As discussed in the overview, this will just prevent client-side JavaScript from accessing the response to a malicious request. It doesn't necessarily stop the server from responding, which is the essence of CSRF attacks.
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.
"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.
CSRF tokens
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.
Acknowledgements
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!