> 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.
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.
> 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.
Idempotency just means no state mutation on subsequent request.
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"}
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.
It's the textbook definition of idempotence. So much so that wikipedia's article on idempotence mentions it and provides links to primary sources.
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.
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
lol
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?
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.
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.
Including me TBH.
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.
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.
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.
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.
It’s a mechanism to allow pages to access servers that they can’t by default - with the permission of the server operator.
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.
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.
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.
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!
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.)
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?
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.For adhoc dev tools, you could also just disable CORS: https://berkkaraal.com/notes/other/chrome-disable-cors/
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 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)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!
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.
(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)
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.]
If that's the case, then yes, the forms method would be 'better'.
Are people using JSON parser as proxy for access control? "Payload successfully parsed as JSON, therefore you are allowed to use this endpoint"?
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 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.
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.
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...
It does, though. See my reply at https://news.ycombinator.com/item?id=48618539 .
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.
> 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...).
> 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.
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.
If a browser does not implement CORS protections (but allows cross-origin requests), then its users must have non-standard expectations about security.
> 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.