Hacker News new | past | comments | ask | show | jobs | submit login

I have some basic understanding of it. I'll try to simplify what I know if anyone else is feeling confused about CORS.

- You visit evil.com.

- Evil tries to makes an HTTP request to bank.com/transfer.php

- But before the request goes through the Browser says, hey, wait a minute I first need to make sure if evil.com can access bank.com

- Browser asks bank.com, if evil.com (also known as origin) is allowed by sending an "OPTIONS" request first (this is before making the actual request)

- Bank.com replies in header saying "sure, everyone is allowed" (e.g. my bank), technically by setting a HTTP "Access-Control-Allow-Origin: * " header (or just evil.com instead of * )

- Browser is happy, it makes the requests and you get the data

Of course there is more to this like the server can tell which HTTP methods evil.com can do like "POST", "GET", "PUT", etc using "Access-Control-Allow-Methods". If the browser needs to send the cookies too with the request ("Access-Control-Allow-Credentials"), etc. But that is pretty much the gist of it as I understand.


- You visit evil.com

- Evil tries to make an HTTP request to bank.com/transfer.php

- The browser happily performs the request, authenticated with your cookies, and the bank, having a CSRF vulnerability, happily sends your money to the attacker.

- Since 'evil.com' and 'bank.com' are different origins, Browser refuses to provide the response to evil.com, but the attacker doesn't care, he got the money.

CORS allows you to relax these restrictions, not tighten them.

Now, bank doesn't like these attacks. So they make the legitimate application send an additional custom header, "X-Totally-Secure: true". Despite being a really bad idea, if (big if) the browser follows the standards, this prevents the attack:

- evil.com tries to make the HTTP request as before

- Browser lets it through, as before

- Bank rejects the request because it's missing the magic header

So the attacker adds the header to the request:

- evil.com tries to make a non-standard HTTP request to bank.com/transfer.php, with the header attached

- BECAUSE IT'S A NON-STANDARD REQUEST, browser asks bank.com (as you described, OPTIONS)

- Bank.com replies "wtf do you want I don't know what OPTIONS is"

- Browser refuses to make the request

Unfortunately, the bank forgot that they have a marketing department, that runs ournewbankapp.com, and shows your current balance in the fake screenshot of the app to show how awesome it is. And your bosses' bosses' boss has yelled at the IT department that rolled out the security measure to make it work again. They make ournewbankapp.com send the magic header (including access-control-allow-credentials), but now the OPTIONS request fails. So they teach the web server to respond with "everyone is allowed" (with "access-control-allow-origin: *" as you described) because they're lazy and dumb.

But because browser vendors know that developers are lazy and dumb, the browser completely ignores this: If access-control-allow-credentials is set, the allowed origin must be listed explicitly. The developers give in, and explicitly add ournewbankapp.com to the header, and now it works, but the attack doesn't work.

(part 2 follows)

Ways this story could have ended badly for the developers:

- instead of hardcoding the allowed value, they set it to always echo the value of the Origin header - browsers are powerless against that much stupidity, and can't distinguish it from a correctly configured server that is allowing the request. evil.com sends the request, bank says "evil.com" is allowed, browser shrugs and sends the request.

- instead of using CORS, they could have tried to build their own custom solution that allows ournewbankapp.com. The attacker would have to analyze how it works, and would then most likely find a way to exploit it to perform the attack, while legitimate users with privacy extensions would make the support hotline despair due to countless people complaining "I can't send money".

- instead of adding the custom header, they could have decided to check if "origin" is present, and if so, assume it's a cross-site request and check the origin against a whitelist. This still relies on standard-compliant browsers but isn't the worst idea AS LONG AS YOU REQUIRE AN ORIGIN VALUE, AND TREAT LACK OF AN ORIGIN HEADER AS A FATAL ERROR, BEFORE TAKING ANY ACTION (e.g. the money transfer) BASED ON THE REQUEST. I think can force same-origin requests to include the origin header with some option on fetch(). But if you treat a missing (or "null") header as ok, the attacker will likely find a way to make some browsers send no header, and steal money again. Sending "null" (or some other default value, been a while since I played with it) is easy, I think with some iframe trickery.

- they could have used plain HTTP, exposing them to DNS rebinding attacks even from remote attackers who cannot sniff their user's traffic.

- they could have an XSS vulnerability somewhere on the main site or the whitelisted marketing site, allowing the attacker to proxy his malicious requests through that site (and thus make them use a whitelisted origin). Maybe some long-forgotten kludge designed to proxy requests to third parties that don't support CORS...

Variant of #1 that I've seen not infrequently:

They set it to check a regex for `bank.com` to also allow `subdomain.bank.com`, but inadvertently also allow `bank.com.evil.com`, `evilbank.com`, or similar.

How would DNS rebinding work? I thought DNS rebinding only works if there's some sort of authority that's ambient across hostnames (such as whitelisted user IP ranges, or connectivity to a network), which doesn't seem to apply here. If you do have authority that's ambient across hostnames, my thought of how to protect yourself is to check the Host header.

XSS is essentially completely separate from CORS and CSRF. XSS is a bad vulnerability, worse than CSRF, and you can't expect any type of CSRF prevention to protect against XSS.

How would one handle the situation of having to respond to (authenticated) requests from a web app running on both localhost and a domain-based website?

I've been in this situation before with web-based cross-platform apps using Cordova. As far as I remember, I ended up echoing the origin domain if it matched a set of allowed domains (basically localhost and example.com), since regular expressions or lists are not supported in allowed-origin headers. For session tracking, I used tokens rather than cookies to avoid CSRF issues. Would there have been any better solution for this? I think not.

This is the first comprehensible, and more importantly memoizable, description of CORS I have read. Hope it sticks this time.

I know you know this, but to clarify steps 2/3:

- Evil tries to make an HTTP request to bank.com/transfer.php

If this is a regular HTTP POST (ie submitting a form, and the browser window changes location), the browser will allow it.

If this is an xhr POST, the browser, following the same-origin policy, allows the request, but prevents accessing the response (unless allowed with CORS).

[EDITED with tgsovlerkhgsel's help!]

Even a JavaScript initiated POST request will go through. Blocking it would not make a lot of sense, because the attacker could just use the FORM (possibly in an iframe to keep it invisible to the victim).

It is possible that XHR, or common XHR libraries, default to adding some header that makes it a non-standard request, but a fetch() call works.

In Firefox, open a debug console and run

    fetch('https://otherorigin.example.com', {method: 'POST', body: 'blah'})
You will see two things. In the console:

    Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://otherorigin.example.com/. (Reason: CORS request did not succeed).
In the network tab, a HTTP request.

Replace with a URL of a server you control, or run `nc -lnvp 9999` and replace the URL with, to see that the request is indeed being made.

As the author said... few people understand CORS.

Bad naming strikes again? The message "Request Blocked" means something unambiguous to me. And it doesn't mean "I made the request, but won't let you see the response."

But for XHR only POST requests that meet a lot of constraints (defined here https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Simpl...) will work, right?

Nothing with a Cookie header for example...

That page says that the Cookie header cannot be "manually set". Headers can still be automatically sent, and the browser can automatically send cookies from the cookie jar. So for example this will not send a request with the manually set cookie:

    r = new XMLHttpRequest();
    r.withCredentials = true;
    r.addEventListener('load', function(e) {console.log('loaded: ', this, e);});
    r.addEventListener('error', function(e) {console.log('error: ', this, e);});
    r.open('GET', 'https://www.google.com');
    r.setRequestHeader('Cookie', 'somecookie=somecookievalue')
But this will send a request with the automatically set cookies just fine:

    r = new XMLHttpRequest();
    r.withCredentials = true;
    r.addEventListener('load', function(e) {console.log('loaded: ', this, e);});
    r.addEventListener('error', function(e) {console.log('error: ', this, e);});
    r.open('GET', 'https://www.google.com');
Assuming the user is already logged in to bank.com , the user's cookies will be automatically sent on the request, and the transfer will go through assuming there is no CSRF protection.

TIL. Thank you for teaching me something - I had no idea the request still went through and it was just the response being blocked.

Thank you, I stand corrected! Editing :-)

How hard is it to trigger a regular HTTP POST instead of an xhr POST?

Not hard. Just make an html form with the values you want, then with javascript call


Another option is to use a secure token (https://github.com/OWASP/CheatSheetSeries/blob/master/cheats...) this is a PITA but frameworks like ASP.NET have this baked in so it's not really much of a hassle in practice.

It certainly won't allow me to make xhr POST requests without sending an OPTIONS request first. Are you sure about that?

Amazing. You learn something new everyday. Though it does make sense why blocking it won't serve any purpose as form submissions are going thru anyway.

Also glad I didn't mention jsonp. God knows what the devs at ournewbankingapp would do with that one ;)

Yes, you can send some POST requests without a preflight OPTIONS request: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Simpl...

Actually they can do better than just a custom header with an XSRF token, which is fairly standard nowadays for requests that are authenticated via Session Cookies (Django for example strongly enforces this by default.)

Indeed, this is the canonical way of avoiding CSRF (= XSRF) attacks. I intentionally only explained the header method because I didn't want to confuse readers with another concept, and because it's a really nice example to explain CORS.

https://github.com/OWASP/CheatSheetSeries/blob/master/cheats... is probably one of the best documents discussing this, including the drawbacks of the header method that I used (and warned against) in my example:


Wait. If the resource at bank.com is protected by a CORS filter, the request which is issued at step three will have an origin header, and the CORS implementation is free to issue a 401 at that point. At least as far as I have understood. That would be tightening the restrictions. Am I missing something?

There used to be ways to get rid of the origin header, not sure if this is still the case. You can easily set it to "null", e.g. with some iframe trickery.

As others have mentioned - thanks for this! I thought I understood CORS, but one thing I apparently wasn't clear on was the conditions on which a pre-flight request was made. Thanks for clarifying!

Thanks for this; it helped me.

Very naively, I don't understand why evil.com origin was allowed to make any request to other domains using any cookies/session/identity of the browser/user in the first place.

Why was this accepted standard? I was blown away when I first learned that a request made to xyz.com while in a browser tab showing abc.com would actually complete the request using my identity on xyz.com.

Because you used to be able to host your web site on www.compsci.tech.youruniversity.edu, and put a HTML <form> element in there, that would send the result to myawesomeguestbook.com.

Thus, cross-origin POST requests were allowed, and changing it was impossible without breaking the web. Likewise you can include images or iframes of other origins, allowing GET requests. However, you cannot read the responses! The JavaScript APIs just make it more convenient to make these requests, but they don't change what you can do - you still can't read the response. Not allowing it in the JS API would just make the attack more annoying (fall back to the legacy methods) and limit legit use cases.

It's important that you are not allowed to make arbitrary requests: As soon as you start using advanced features like custom headers (in other words, things you couldn't do with img tags, redirects, forms, and iframes), you run into preflight (where the browser asks the server whether the request is allowed before performing it, via OPTIONS, i.e. in a way that legacy servers will not understand and safely reject).

My reading, feel free to correct if I'm missing something.

This is a compatibility feature that can never change, and shouldn't change, because we can't break the web. But my reading is that CORS is mostly a band-aid on what was a bad security policy to begin with.

The problem isn't that a browser might let a web page on a separate domain send and read any arbitrary request to another domain without permission -- with the exception of DDOS and bot attacks against servers, it's difficult to think of what risks this would actually pose to users themselves. And any native program on your computer can already send an arbitrary request to an arbitrary domain and read the data -- so there are at least few arguments that it might even be beneficial for websites to be able to do the same.

The security problem is that requests are made with your current cookies regardless of what domain you're on. Ideally, browsers from the start would have isolated every domain into their own container, so that if evil.com made a GET/POST request to mybank.com, it wouldn't be authenticated as me, regardless of how the server was configured.

We made it so that cookies could only be read by certain domains, but we didn't think until very recently to make it so that cookies could only be sent by certain domains. And at this point, too much infrastructure relies on this behavior for us to ever come up with a better model, so we're stuck with hacks like CSRF tokens which are dependent on websites being coded well.

>The problem isn't that a browser might let a web page on a separate domain send and read any arbitrary request to another domain without permission

That's not fully correct. Even without cookies it's bad if a.com can read a response from b.com . Why? Because b.com might be hosted on a private network, and contain private information. We don't want a.com to steal that information. Even if cookies aren't sent, there can be other types of ambient authority, and connectivity to a private network is one of those types of ambient authority. Another similar type is if a website is protected using a whitelist of user IPs. Side note: any website that uses this type of ambient authority that isn't associated with a hostname must also check the Host header to protect against DNS rebinding.

Of course if the web was built from the start such that a.com can read b.com just without cookies, then such websites that rely on non-host-associated ambient authority (hopefully) wouldn't be built and you would be right.

Just because a native program can do something doesn't mean a website should be able to. A native program can delete all my files without asking me. A website can't.

The alternatives are grim.

As an extreme example, if I were to stream video to your browser from my website, evil.com, I would have to either host or proxy those streams through my server. Instead, I can just point your browser to a video from youtube.com or something.

If youtube.com then decides you need to log-in (or be logged-in) to verify your age (as they do), then that is between your browser and youtube.com to negotiate -- evil.com never sees your credentials.

A tangent, but YouTube actually does not require login to verify age on embedded videos.

You're welcome. Yes you still can make GET requests from evil.com to bank.com/transfer.php by setting the image src or script src or form submission though you won't be able to read the response.

This is probably the reason that any url that makes changes to user data like logout.php, transfer.php, etc must be a POST request (with csrf token to protect again requests created via form submissons)

Also it's still possible to bypass CORS using JSONp requests or for submisson, but for that transfer.php must support jsonp too (by setting the callback method)

Wow that makes more sense too. Basically, don't implement GET in any remotely-sensitive API that actually causes changes in the remote system/service?

EDIT: Why isn't there a header that web servers can give out to browsers basically saying "Don't use the cookie/session I'm giving you ever, unless you're literally on this site"? I could see this being very useful for banks or other origins where they expect no requests except from direct users.

"Why isn't there a header that web servers can give out to browsers basically saying "Don't use the cookie/session I'm giving you ever, unless you're literally on this site"?"

There is, it's relatively new though:


Yes also see my comment about csrf token too. If the request changes user data it must be a POST request with csrf protection.

You can't make POST requests with ajax as CORS will protect you, but evil.com can create a fake form submission (with bank.com/transfer.php as the form's action) which can be a POST request. So in this case you match the csrf token which only you know and which is unique for the form/session.

It's a rabbithole :)

It seems you can make a POST request from javascript, as long as it doesn't have non-standard headers or content-type.

Try it yourself: https://news.ycombinator.com/item?id=20406633

By default, under the same origin policy, a browser won't allow requests cross origin.

But there are valid situations where you want a request from 1 domain to be made to other domains. This is where CORS comes in.

CORS is a mechanism to loosen security, not increase it. It allows a server to say, these are the domains (outside my own domain) who can make requests. CORS headers should be set carefully so that you are only allowing the domains that should be allowed through.

> CORS is a mechanism to loosen security, not increase it.

Or we could call it CORB instead (Cross origin request blocking), and then we see it's a mechanism to tighten security. Since fundamentally, what we have is an agreement against major web browser vendors that blocks cross origin requests unless the web server authors have used CORS.

I mean, how many people have encountered a problem with CORS? Almost no-one, and those that have encountered a problem with CORB and solved it by enabling a shitty CORS that opened the doors. (At least, they're fixing security holes in software that was written by devs who encountered a CORB problem and fuxed it. But all CORS problems follow a CORB problem.)

If we called it by its true name, maybe it would help people understand what's happening. Names are important. If developers understand CORB, they will potentially understand CORS. But no-one can understand CORS till they've understood CORB.

I think you have a bad acronym collision with Cross-Origin Read Blocking.


> By default, under the same origin policy, a browser won't allow requests cross origin.

Save a rather short-but-impactful list of exceptions.

> CORS is a mechanism to loosen security, not increase it.

Would that everyone shared your understanding.

Add in these two insights to those we are enlightening:

* CORS is enforced by the browser, so no, your curl command working doesn't say your service is fine

* That error message in the browser about 'no-cors'? It is 99% likely that no-cors is NOT what you want, so the error message is just misleading and unhelpful

...and you'll have covered my CORS wishlist :)

> By default, under the same origin policy, a browser won't allow requests cross origin.

Cross origin requests are allowed (as long as they're simple). Reading the response is what's blocked.

This context was incredibly useful; thank you.

Am I missing something or is this transaction actually a browser being the deciding factor for whether or not the request gets sent?

If that’s true, couldn’t a nefarious browser decide when to push a request and completely ignore the OPTIONS header?

Yes, and that is a very important point you noticed.

Both CORS and CSP are for guarding against malicious code on the web, being executed by non-malicious standards-following browsers used by non-malicious users. (the user is in fact who is being attacked).

I think is frequently confused, and it leads to a mistake in the other direction, people thinking CORS or CSP can guard against malicious user-agents operated by malicious users. It is not for that!

Yeah that's correct, CORS is a browser feature. If you had a nefarious browser installed it could indeed defeat CORS, but at that point you already have a nefarious browser and CORS is the least of your concerns.

CORS prevents a malicious site from exfiltrating/accessing data using the access you have, for example an internal site that is on your computer's network but the malicious website's servers can't directly access.

Of course, anyone can use netcat to send any kind of request.

However, this attack is most useful if you can get the victim's browser to send the request for you, because that way, you can get it to include the victim's authentication cookies.

If you as the attacker send the request yourself, you don't have the cookies.

If you make the victim's browser send it, you either can't make them use a nefarious browser, or you already won since you have code execution on the victim's machine.

Sure, but then again a nefarious browser could just record and broadcast all your interaction (including password etc) with bank.com directly.

No web standard can protect you from a nefarious browser, since the browser could just decide to not follow the standard.

You're correct, the browser is the safety net when it comes to CORS.

Safari used to let you turn off CORS checks in the developer tools menu.

Registration is open for Startup School 2019. Classes start July 22nd.

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact