Using CORS policies to implement CSRF protection

SHARE ON

This post is a follow-on to our CORS post back in December. We’ll describe howtraditional 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:

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 theRefererdoesn't leak to third-party
websites. We can prevent such leaks with the relatively new
Referrer-Policyheader. 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 jeffwear 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!

SHARE ON

Written By

Eli Skeggs

Eli Skeggs

From Your Friends At