Even TFA seemingly doesn't understand CORS. Or at least misreprents it grossly:

> The webserver listening in on localhost:19421 should implement a REST API and set a Access-Control-Allow-Origin header with the value https://zoom.us. This will ensure that only Javascript running on the zoom.us domain can talk to the localhost webserver.

No, that does not do that. JavaScript from any other website can still talk to localhost:19421 just the same. CORS doesn't restrict anything, it loosens the default set of restrictions (ignoring preflight requests for now and assuming we're talking just about "safe" Methods). That Access-Control-Allow-Origin header just allows JavaScript running on zoom.us to read the responses when it queries localhost:19421. The requests happen in any case, and you must ensure in your backend that they don't cause any adverse effects.

I don't understand why this is the most upvoted comment. OP is right, and you are wrong.

> The requests happen in any case, and you must ensure in your backend that they don't cause any adverse effects.

GET requests will be sent, but they are supposed to be idempotent so if your server is implemented in a sensible way, it cannot cause any adverse effect, and reading the response is all that matters for GET requests.

For non-idempotent requests however (the only ones that are supposed to be able to have side effects), a preflight OPTION request will be sent in cross-origin context instead of sending the request itself. And unless the right headers are set in the OPTION response, the request won't be sent at all.

> GET requests will be sent, but they are supposed to be idempotent so if your server is implemented in a sensible way, it cannot cause any adverse effect, and reading the response is all that matters for GET requests.

This is not correct. Safety and idempotency are two different concepts. Safety is when a request does not result in a state change. Idempotency is when the outcome is the same whether you make the request once or several times.

GET is defined to be both safe and idempotent. Clients can make GET requests without explicit human intent driving them because they are safe, not because they are idempotent.

DELETE is not defined to be safe but it is defined to be idempotent. This means you need human intent to drive it, but clients can retry as much as they like because whether you ask to delete a resource one time or a hundred times, the result is still the same – the resource is deleted. Obviously making DELETE requests can result in adverse effects even though it is idempotent.

POST is neither safe nor idempotent, however it’s subject to some unintuitive rules when it comes to things like cross-origin requests for historical reasons.

I wasn't aware of the distinction between “safe” and “idempotent”, TIL, thank you.

> DELETE is not defined to be safe but it is defined to be idempotent

This one puzzles me though. How can DELETE be idempotent? If the first request works then the second one should return a 404, as the key to delete doesn't exist anymore.

HTTP requests are not RPC calls. The end state of a DELETE request is that the resource does not exist. If you make the request once, the end result is that the resource does not exist. If you make the request twice, the end result is that the resource does not exist. The spec. allows for the actual response to differ; the important thing is that the state is the same regardless.

Response is implementation detail: a 404 on second request is one way to do it, a 202 would be another, or 200 with some sort of response to distinguish (e.g. { changed: boolean }).

Idempotency just means no state mutation on subsequent request.

> 202 would be another

Then you'd return 202 on a request to delete any invalid element, which really doesn't sound right.

> 200 with some sort of response to distinguish (e.g. { changed: boolean }).

Sounds even worse, and much like APIs that returns 200 with a payload saying {error:"not found"}

> Then you'd return 202 on a request to delete any invalid element, which really doesn't sound right

It depends on your use-case if you care about this or not. If you do care you will have to handle it either way somehow.

But if you want an idempotent API then a success is more appropriate IMO than an error.

Request failed successfully

It's the graphql way.

> This one puzzles me though. How can DELETE be idempotent?

It's the textbook definition of idempotence. So much so that wikipedia's article on idempotence mentions it and provides links to primary sources.

That's not quite correct. POST requests with certain Content-Type headers, such as text/plain or multipart/form-data, will still be allowed without any kind of preflight. If the web application doesn't check the Content-Type header strictly, then you've got a problem.

You're right, I forgot that form requests bypass the preflight.

We are saying the same things, just expressing them in different terms. Yes, only idempotent methods and "safe" POST requests don't need preflight. And I'm saying you need to make sure in your server implementation that those are actually safe. The article states that the CORS header will prevent random other websites from talking to localhost at all, which is just wrong.

You are kind-of both right. The spec defines a subset of cross-domain requests called “simple requests” - basically such requests as has always been supported by a plain html form. These are not affected by same-origin or CORS. So you can post url/form-encoded data to a different domain - but you cant access the response.

But CORS affect all other requests, e.g POST using JSON or XML content type, and all other methods like PUT, DELETE, PATCH.

So you can do an unsafe POST using form-encoded data, but if a server supports this, they hopefully mitigate CSRF, since this has always been a risk.

>GET requests will be sent, but they are supposed to be idempotent so if your server is implemented in a sensible way, it cannot cause any adverse effect, and reading the response is all that matters for GET requests.

Just my first thought as a security engineer, but sounds like a perfect opportunity to execute a timing attack to me. For example, vheck which users exist (by measuring response time for /api/users?name=john) etc

I encountered many a web service that do not use HTTP verbs correctly.

> if your server is implemented in a sensible way,

lol

I don't think you can even say CORS does that.

The degree to which CORS is poorly understood (I have read numerous (often contradictory) documentation and I don't really understand it.) means that you can't rely on it being implemented properly by an unknown party,

If a protocol reaches this level of widespread confusion, I think all bets are off. Even if one end of a system performs correctly, who's to say that the other will. If people adapt their code until it works with another implementation, were they mistaken, or the other end?

I think it still works, because the number of "client implementations" of CORS is very limited (*) - only the browsers have to implement that, and the browser devs seem to understand it well enough.

So there is only one end of the system that is confused - the servers - but at least the other end - the browsers - can mostly be trusted to implement it correctly.

(*) unless you're implementing an open proxy, but then you have bigger problems.

There’s few implementations of the engine, but many implementations of rules for that engine.

I think OPs point is that many of those rule sets probably don’t do what the author intended.

I would second that, because CORS questions are common and “turn on the allow all pattern” is almost always in the top 3 suggestions.

Semi-related tangent, it annoys the hell out of me that create-react-app (and the newer incarnation) don’t come with an “allow all” CORS rule. Don’t force me to figure out which arcane setting configures CORS headers, I’m the one writing the code, I’m okay with wherever the HTTP requests are going, I’ll set up real CORS headers on nginx for prod.

It seems like nobody understands CORS.

Including me TBH.

I don't understand it. And I'm a web developer. I don't understand the documentation, I don't understand the problem it's trying to solve and I don't understand how it's going about a solution of any problem. The closest I've come to an understanding is that it's meaningless make-work for a ledger of http calls that are not giving any security.

Back in the day when I started web development, websites making their own requests after they loaded wasn't a thing. Eventually, XMLHttpRequest appeared, which let JS do HTTP requests at (page) runtime, and the whole "AJAX movement" kicked off.

Initially, you could literally hit any website with any sort of request, so your website.com could make requests to bank.com, and the browser happily obliged. Of course, this opens up a whole host of issues, so browsers started limiting websites to just being allowed to make requests to the same Origin. But sometimes we want to be able to make requests from pages to other Origins, so CORS (Cross-Origin Resource Sharing) lets you configure your server to tell browsers that "You're allowed to make requests to me, even if you're on a different origin".

This is basically the simplified version of the why and how behind CORS.

> Initially, you could literally hit any website with any sort of request, so your website.com could make requests to bank.com, and the browser happily obliged. Of course, this opens up a whole host of issues, so browsers started limiting websites to just being allowed to make requests to the same Origin.

I think that’s overstating it a bit. JavaScript was introduced in Netscape 2.0 and the SOP was introduced pretty much straight away – Netscape 2.0.2 I believe. Almost 20 years passed and then CORS was created. So while it’s technically true, the timeframe in which JavaScript could make any cross-origin requests was basically the blink of an eye, and for all intents and purposes, the SOP has been around since the beginning and definitely many, many years before Ajax came around.

Yeah, definitely I was simplifying a lot, borderline misleading perhaps even.

Before XMLHTTPRequest there was also a time we were doing requests via ActiveX as well, but I did it so briefly I barely remember how it worked by now, and I'm 99% sure this was exclusively in IE as well, maybe IE4 or IE5. I'm not sure if the issue mentioned earlier with cross-origin requests may have been exclusive to IE as well, but I think there was a larger window than "blink of an eye" that it was a issue.

But again, this is all long time ago, and it was in the beginning of my career, I might misremember and you may very well be right.

XMLHTTPRequest was originally an ActiveX object (something like ActiveXObject("Microsoft.XMLHTTP")), that’s probably what you are thinking of. You couldn’t make cross-domain requests with it though. Other browsers then implemented XMLHTTPRequest based on the ActiveX object, and then Internet Explorer supported XMLHTTPRequest and dropped ActiveX.

Before that, people who wanted to make cross origin requests sometimes used Flash but I think that always needed a crossdomain.xml file to work. JSONP was also used, which is where you source a <script> from the remote that calls a function in your own context to pass information in. You needed to be a little more careful with that, but only because you were deliberately passing information in; the browser couldn’t read it by itself.

I’m pretty sure the SOP has been effective in all non-Netscape browsers from as soon as they started supporting JavaScript.

[deleted]

CORS isn’t designed to increase security, since the same-origin policy is a secure default.

It’s a mechanism to allow pages to access servers that they can’t by default - with the permission of the server operator.

Yeah, basically Same-Origin Policy (https://en.wikipedia.org/wiki/Same-origin_policy) was the part that increased security, as it prevented websites (in browsers) from making arbitrary requests to arbitrary 3rd party websites.

Cross-Origin Resource Sharing (https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) is one way to relax the Same-Origin Policy, so you essentially whitelist what actually can be shared across Origins. To be used when the default Same-Origin Policy is too strict.

Overall I think it's a really simple concept, but libraries/frameworks/docs seems to constantly over-complicate it with their explanations.

But the combination of the two reduces security in the same manner as absurd password requirements cause people to write down their passwords.

A strong security measure without a reliable way to do the things you want to do induces people to bypass the security altogether.

Security designers generally are ok with this because they consider usability or user behaviour to be not their responsibility.

If you're hosting some 3rd party api that's safe to call client side then you send some header that says so. The problem is when it's not safe and devs try to bypass (a reliable way to do the things they want).

The solution is to convince devs to not want to do those things.

CORS allows JavaScript to make requests to different domains than the origin of the page. By default (without CORS), JavaScript can only communicate with the origin domain.

A poorly documented, poorly implememented, and poorly understood protocol is a worthless protocol. More than that, it's a potential attack surface, and the idea is to reduce those. If you are the admin of something, and you are putting things into production in which you don't fully understand the implications, because you copy/pasted some crap from stackexchange assuming the person that posted it knew what they are talking about, then you are doing it wrong. Just look at this thread. It's chaos and reinforces the fact that even people that think they know, don't really know. When in doubt, grab the RFC and figure it out.

> A poorly documented, poorly implememented, and poorly understood protocol is a worthless protocol.

The world seems to manage just well to get CORS to work, though. If developers fucking up implementations of any standard is enough justification to argue that something is worthless, you'd be hard pressed to find any software engineering topic that by your personal definition would be deemed worthless.

> When in doubt, grab the RFC and figure it out.

Back in the day, I was using Cloudhopper, a Twitter-developed library for the SMPP protocol (not to be confused with SMTP!). Protocols being protocols, there are strict limits on field sizes, defined on the actual protocol spec. I noticed that Cloudhopper didn't impose those limits, however.

Long story short, it turns out they just left out strictly imposing field limits because other implementations didn't care either. De facto has overruled de jura and the inmates are running the asylum!

My understanding was that "preventing otherwise disallowed HTTP requests" was the entire point of the preflight OPTIONS request, and that CORS will do nothing if the request would otherwise be allowed.

For example, a POST request with a Content-Type of "text/json" would not be allowed to be sent to third-party hosts without an OPTIONS preflight, but one with a Content-Type of "multipart/form-data" would be allowed and wouldn't be stopped by CORS at all, even to third-party hosts.

(And, of course, if your endpoint just assumes JSON without strictly checking the Content-Type, then congratulations, you've just allowed any website to POST to you, with no user action required.)

> (And, of course, if your endpoint just assumes JSON without strictly checking the Content-Type, then congratulations, you've just allowed any website to POST to you, with no user action required.)

Is that so? Neither urlencoded forms nor multipart/form-data are valid JSON on the wire, so while other websites could send requests, wouldn't they just hit a parse error?

You can massage a text/plain form into valid JSON. text/plain is also one of the allowed default types. It works if the server doesn't check the content-type.

Source: I've done that successfully in multiple pentests.

Edit: lazy LLM generated example:

  <form action="https://example.com/api" method="POST" enctype="text/plain">
    <input name='{"key":"value", "ignore":"' value='"}'>
  </form>
That gives you

  {"key":"value", "ignore":"="}
The trick is to stuff the = character you cannot control into an irrelevant value.

Ouch! Ok, I wasn't aware text/plain was also one of the whitelisted values and lets you do that. That looks pretty bad indeed.

Or good, depending on who you are. I find CORS to be pain in my butt by effectively preventing ad-hoc client-side browser tools from being able to use most APIs, and forcing me to do bullshit make-work involving a server side component and certificates just to work around this + HTTPS, at which point I may as well just have my "backend" shell out to `curl`, which happily doesn't care about any of that nonsense.

I mean, it still relies on a vulnerable configuration (not checking the content type)

For adhoc dev tools, you could also just disable CORS: https://berkkaraal.com/notes/other/chrome-disable-cors/

> I mean, it still relies on a vulnerable configuration (not checking the content type)

Unless I'm misunderstanding the claim, that's not "vulnerable configuration", but extreme lunacy - basically treating parsing outcome as access control. "Did the payload parse correctly as JSON? No -> go away; Yes -> oh that must mean you're supposed to be here". I'm at a loss of words that this is even a problem in practice.

> For adhoc dev tools

There's a whole space between "adhoc" and "dev" tools, though, and this is what interests me the most. Yes, when I'm in full dev mode, I can make my computer do anything I need to. But more often than that, I'm just a user that wants to exercise some basic freedom of computing - to remove some toil or frustration from daily computing experience - without switching my hat from "user" to "developer". That's what CORS has been successfully defeating, by forcing any ad-hoc non-dev tool I could make for myself to require being in "developer mode" to use it.

This is fundamentally a CSRF issue and framing CSRF as an access control issue often yields to wrong conclusions. With CSRF you might face the situation that the request has a valid session cookie, but is actually created by an attacker coercing the victim's browser into sending a request unbeknownst to the victim.

This case gets more and more complicated with browser defenses such as SameSite cookies or fetch headers you can use to mitigate this case, but let's ignore that for now.

To drive my point home, similar to how ensuring the content-type is set correctly on your JSON endpoint prevents CSRF, it's actually also a very real defense to require a custom header to be set, e.g.

  I-Promise-To-Not-Be-Malicious: true
Requiring this header will prevent CSRF because browsers won't allow you to set that cross-origin (unless of course you allow anyone to set it via CORS)

Regarding the first part, it's easier than you might think to have a false sense of security.

I've seen a web application that did, in fact, check the Content-Type header to make sure that "application/json" was there - but it didn't check that the header value started with that. That meant that setting the header to "multipart/form-data; boundary=application/json" was enough to bypass a CORS preflight!

[deleted]

If your web application specifically parses data based on the Content-Type that it advertises itself to be, then yes, the webapp would hit a parse error. But there are many applications that don't do that.

An attacker might use JavaScript to set a "multipart/form-data" Content-Type (thereby bypassing the otherwise required OPTIONS preflight), but send JSON in the request body. Unless your web application specifically parses the body based on the Content-Type (web servers don't do this for you), then you wouldn't detect that.

Well, if your endpoint expects JSON, then at some point it will have to parse it. Even if it completely ignores the content-type header and simply always passes the request body to the JSON parser, the parser would throw.

(But I was wrong, there are ways to produce request bodies that are valid JSON even if the browser forces you into a different format, as the sibling comment demonstrated)

> ...there are ways to produce request bodies that are valid JSON even if the browser forces you into a different format...

The browser basically never forces you into a particular format. You don't even need to do the trick with the form stuff that the sibling was talking about. Consider the following JavaScript:

    var xhr = new XMLHttpRequest();
    var url = "http://localhost:12345/endpoint";
    xhr.open("POST", url, true);
    xhr.setRequestHeader('Content-Type', 'multipart/form-data');
    xhr.send('{"hello":"world"}');
No trickery required, it just does it.

[Edited to illustrate my point better.]

You can do that, but my understanding is you can't get the browser to attach cookies to your request in this way, while you can with forms. Do you agree?

I haven't actually investigated that (and I'm not able to do so right now), so I couldn't tell you for sure.

If that's the case, then yes, the forms method would be 'better'.

[deleted]

Interesting. Is this still sent as a "safe" request though or does it trigger a preflight request etc?

If it was one of the requests that would trigger a preflight normally, then yes, it would trigger a preflight. But the code as shown doesn't do that because "multipart/form-data" is one of the allowed MIME types that can bypass these preflights.

Obviously JSON is a subset of text/plain, so I don't know what people were expecting? For text/plain to mean "plaintext, excluding any string that could possibly parse as any of the other named formats that have a plaintext representation"?

Are people using JSON parser as proxy for access control? "Payload successfully parsed as JSON, therefore you are allowed to use this endpoint"?

If the JSON backend parses the payload as JSON without verifying the content type, then yes, it's a way through. There's no affirmative logic of "payload looks like json, so let it through" going on, it's just "parse this json without looking at content-type, and assume it was already subject to cross-origin restriction because it's json, right?" (same thing, just different intentions). And as we can see, that assumption fails to hold if you don't actually check the content-type.

My apps speak only JSON, so one of the first things I do is create a middleware that requires any POST/PUT/PATCH request to be application/json and reject everything else with a 415 error. That's so I can turn off the CSRF protection mechanics in the framework completely, but the two concerns are related.

I wasn't aware that plaintext was one of the whitelisted types that are allowed without a preflight request.

I guess the same trick might work with urlencoded forms, but it wouldn't work with multipart/form-data

> Are people using JSON parser as proxy for access control? "Payload successfully parsed as JSON, therefore you are allowed to use this endpoint"?

For better or worse, yes, or at least as one layer. That's one of the rationales behind the "safe" requests AFAIK.

And this wouldn't be the first time, protocols are made intentionally incompatible on the wire, so an attacker can't smuggle one inside the other. That's the entire reason for WebSocket's weird handshake dance and the "xor encoding" it applies to messages from the client.

> I wasn't aware that plaintext was one of the whitelisted types that are allowed without a preflight request.

Until now, I wasn't aware of that either. My response is about the fact you can massage the plaintext part to contain valid JSON somehow being a problem, one that apparently is a security issue in practice.

We're not talking about some clever polyglot quine like those COM executables that are somehow also valid Bash and C code and PDF files or something. text/plain is a superset of everything that can be represented by plain text, which includes approximately all code and data formats, JSON and XML included.

> And this wouldn't be the first time, protocols are made intentionally incompatible on the wire, so an attacker can't smuggle one inside the other.

I need to learn more about it, thanks for pointing it out.

Though at the surface, it reads to me like removing a feature. "Smuggling a protocol inside the other" sounds to me like an important feature, or perhaps more accurately, I find myself being part of the "attacker" population much more often than not. "Tunnel $whatever through HTTPS because corporate/ISP firewalls" is both a meme and success story for plenty a SaaS at this point.

> text/plain is a superset of everything that can be represented by plain text

Not in the context of web forms.

Just checked the spec and "text/plain" just seems to be an alias for "application/x-www-form-urlencoded" [1] - i.e. stuff that looks like

  key=value&anotherkey=anothervalue
on the wire.

Apparently though, keys and values can contain arbitrary characters and arent percent-encoded, so you can do a "quine" where the "key" is

  {"foo": "bar", "ignore": "
and the "value" is

  "}
And then the browser will happily send

  {"foo": "bar", "ignore": "="}
over the wire, which is valid json.

[1] https://html.spec.whatwg.org/multipage/form-control-infrastr...

> I guess the same trick might work with urlencoded forms, but it wouldn't work with multipart/form-data

It does, though. See my reply at https://news.ycombinator.com/item?id=48618539 .

> assuming we're talking just about "safe" Methods

That's a pretty big assumption. Any decent webdev should not let GET/HEAD/OPTIONS modify state (joining a meeting is changing state) and additionally PUT/DELETE should also be idempotent.

POST with JSON (or other non-form formats) api's should also have it's content-type header checked (text/plain forms can send a JSON body but the content-type will be text/plain). PUT/PATCH/DELETE and POST with a non-form content-type (application/x-www-form-urlencoded, multipart/form-data, or text/plain) will trigger a preflight so that CORS is properly checked before the actual request reaches the server.

> Any decent webdev should not let GET/HEAD/OPTIONS modify state

> additionally PUT/DELETE should also be idempotent

Yes, but I think the majority of large web applications are not fully correct in terms of 'Safe and Idempotent Methods' (https://datatracker.ietf.org/doc/html/rfc9110#name-common-me...).

That's because they're badly written by people that don't understand that REST doesn't mean JSON+POST+GET.

Another quote from the article:

> Further, native apps can generate a unique self-signed certificate.

Just creating a certificate will not work, unless it's installed as root CA certificates in all browser trust-stores on the machine. And if the private key of the root CA is not secured correctly, one could MitM any websites. So at least you want it name constrained (https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1....), but at least in Chrome until 2023 (v112) that did not work on root CA's (https://alexsci.com/blog/name-non-constraint/), so you had to add an intermediate CA and add the constrain there. Of course, you should also just throw away the key of the root CA.

I will admit I once added basic constrains in some project with a local root CA (2020-2022), but 'incorrectly' to the root CA, and did not test it in all browsers.

> (ignoring preflight requests for now and assuming we're talking just about "safe" Methods)

You can't ignore those because they constitute the bulk of CORS' security model.

Yes, you're technically right that CORS cannot prevent other websites from making any request to your server - this would be impossible, since the browser somehow has to get the CORS headers in the first place.

However what CORS absolutely lets you do is prevent requests to particular endpoints - and you can then design your API in such a way that the dangerous actions are only available behind those endpoints and thus make it safe.

I.e. what's missing in the TFA quote is that the server must also change the endpoint from GET to POST (in addition to setting the CORS headers) and remove the GET endpoint. Other websites would still be able to send a GET or a preflight OPTIONS request, but they wouldn't be able to send the actual POST request.

As such, Zoom's workaround had two problems: They didn't set any CORS headers, which prompted the browsers to only allow "safe", i.e. GET requests - and then put an unsafe action behind the endpoint, therefore violating the "safe" assumption. Moral of the story: Don't put actions that do something else than returning a result behind a GET request.

Every ad GET is doing a lot of things that violate that edict.

Yes, but if ads do it, that would at worst make the ad server vulnerable, not your server.

You apparently understand less than me. The little I do understand is that CORS is protection for a site's user, not the site.

It's a protection of site's business model.

Importantly it only prevents clients that actually cares about the cors headers. Like ohh I'm from hacker.org and the http headers says it only allows zoom.us ohh nooooo. Like it's just a http header! Now if you use a mainstream browsers and you accidentally visits hacker.org in a iframe at some shady site - then the cors header will prevent your browser from accessing it.

It is widely assumed by users that web browsing is safe.

If a browser does not implement CORS protections (but allows cross-origin requests), then its users must have non-standard expectations about security.

“Can still talk to, but cant read the response” is a bit too simplied. You can’t post a json payload for example, which is how a JavaScript client would usually talk to a backend. You can only post using form data encoding, since this is already possible using a plain html form without any JavaScript. Anything beyound that, like json/xml payloads or methods other than post and get are blocked by default.

>> This will ensure that only Javascript running on the zoom.us domain can talk to the localhost webserver.

> No, that does not do that.

It restricts non-zoom.us domains to CORS-safe operations.

Which sometimes includes making the request, sometimes includes reading the response content or headers.