Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSP and HTML Modules #544

Open
dandclark opened this issue Jan 25, 2019 · 9 comments
Open

CSP and HTML Modules #544

dandclark opened this issue Jan 25, 2019 · 9 comments

Comments

@dandclark
Copy link

My colleagues and I on the Microsoft Edge team recently shared a proposal for HTML Modules as an extension of ES6 JavaScript modules. HTML Modules allow for access to declarative content from within a script module. Our Explainer Doc fleshes out this idea.

One developer pointed out that our use of inline scripts in HTML Modules could interact poorly with CSP’s unsafe-inline source expression.

An HTML Module uses inline scripts for two primary purposes: to specify its exports, and to import other modules (script or HTML) that it depends on. The reason that inline scripts in particular are required is that a non-inline module script can be imported from multiple contexts, making its referrer document ambiguous, so it’s not clear how a non-inline script would interact with its referrer to specify exports of HTML content.

An HTML Module’s inline scripts are different from normal scripts in a few ways. Firstly, only type=”module” scripts are allowed, so there’s no synchronous execution during parsing. Secondly, their execution isn’t queued up by the parser as per normal deferred scripts. Instead, they are incorporated into the module graph as the required modules of the HTML Module, similarly to how a script module’s required modules consist of the set of modules requested in its all of its import {…} from “…” statements. As such they are executed only after the entire module dependency graph has been fetched, as part of the module graph evaluation when all the modules in a module graph are executed in order of a depth-first post-order traversal.

So, inline scripts in HTML Modules don’t have the same behavior as normal inline scripts. Another way to think about them is that they are declaratively specified script module imports of the HTML module. Additionally, it's impossible to dynamically inject a script into a module in such a way that it will execute; only scripts that were parsed with the original HTML Module are included in the module graph and executed.

Given the above, we believe that inline script elements included in HTML Modules should be allowed to run even without the presence ‘unsafe-inline’ in a script-src directive.

This decision clearly merits input from the experts. Does anyone in the WG have concerns with this? Are there any dangers here that we are missing?

Also worth noting is that we would propose to have the importing of HTML Modules governed under the script-src directive in the same way that it applies to importing of script modules.

One more question we're considering is whether or not an HTML module (which is a document) applies CSP directives specified in its headers when it is being processed as part of the module graph. We're thinking the answer is no -- that only the "root" document's CSP directives apply -- but we're not sure. On the one hand, if I want to consume a module hosted on a third-party server I shouldn't have to worry about whether the server’s lax CSP policies could compromise the security of my application. On the other hand, it seems unintuitive that a text/html response’s CSP is respected or not based on whether it’s getting pulled in as a module vs. loaded as a top-level document. Feedback on this is welcome as well.

We’ve also been considering the issue here regarding module imports in a nonced inline script, but this seems more applicable to ES6 modules in general with no special consideration needed for HTML Modules.

@annevk
Copy link
Member

annevk commented Jan 28, 2019

only scripts that were parsed with the original HTML Module are included in the module graph and executed

That's still the same basic problem though, no? If you find some kind of XSS (granted, it's more involved as there's multiple steps) an attacker gets to influence that.

I guess the main question if we see these as subresources that are similar to other subresources, or subresources prone to XSS due to their HTML-y nature.

@annevk
Copy link
Member

annevk commented Jan 28, 2019

(FWIW, script-src makes sense, and I don't think CSP policies delivered with a module script should apply as they don't for "normal" module scripts either.)

@shhnjk
Copy link
Member

shhnjk commented Feb 10, 2019

I guess the main question if we see these as subresources that are similar to other subresources, or subresources prone to XSS due to their HTML-y nature.

I think HTML Modules isn't prone to XSS. Because it's not meant to dynamically generate HTML according to user input. Yes, there might be a DOM based XSS depending on inline scripts, but that's same as subresources like JS file.

@arturjanc
Copy link
Contributor

There is some precedent for this in how HTML imports interacted with CSP. AFAIK they applied the loading document's script-src restrictions to both the original HTML import and to any scripts inside the import. @mikewest do you remember what the original reasoning behind this was?

The main concern here is that HTML is frequently constructed unsafely, with user data interpolated without sufficient escaping; we can hope that HTML modules will be static as @shhnjk says, but there is no reason an application couldn't use user-controlled data when returning the contents of the module, leading to an XSS that wouldn't be subject to CSP, resulting in a security regression. OTOH we can also treat this more similarly to ES modules and assume that, once loaded, the module gets the ability to execute scripts -- I don't think that's necessarily a wrong way to look at this.

No concerns from me when it comes to inheriting the loading document's CSP. One thing to keep in mind in this model is that we should tell developers to always also set a CSP on the HTML module response. An attacker can navigate the victim to it directly so if there's a DOM XSS in a script in the HTML module, it would execute in the context of the hosting origin (which is difference between HTML modules and ES modules / JS subresources); developers would need to have CSP set on such responses even if the policy would normally get ignored.

@lmuntaner
Copy link

I understand this might be a stale issue, but I found a different behavior in how Chrome, Safari and Firefox interact with HTML Modules and CSP.

Maybe someone can help me better understand it.

We had an inline script importing a module:

<script type="module">
  import start from './start.js';
  start();
</script>

We used sha256 hashing for our CSP tag:

<meta http-equiv="Content-Security-Policy" content="
  script-src 'unsafe-eval' 'unsafe-inline' https: 'strict-dynamic' 'sha256-GD2MyhNzRFjmxD4jAUnvYIt90C1er46lTotfFdaZ3lg='
">

I found three different behaviors:

Safari:

It works.

Chrome:

Refused to load the script 'http://127.0.0.1:8080/start.js' because it violates the following Content Security Policy directive

Yet, if we preload the module start.js it works:

<link rel="modulepreload" href="./start.js">

Firefox:

It doesn't work in any case.

We had to do a workaround:

We moved the inline script to a file:

// main.js
import start from './start.js';
start();

Then we loaded it dynamically in the html:

<script>
   const loader = document.createElement("script");
  loader.type = "module";
  loader.src = "./main.js";
  document.head.appendChild(loader);
</script>

We had to change the sha256 of the CSP to the new inline script.

Any ideas of why this different behavior?

Thanks!!

@antosart
Copy link
Member

This is confusing, but I believe chrome is following the specification in the example above.

I think the specification prescribes that the static imported script should be fetched with the same options as the parent script. Hence, the imported script is fetched as parser-inserted, hence 'strict-dynamic' does not apply. This is why chrome refuses to load it. Note that 'script-dynamic' makes 'unsafe-inline' and https: to be ignored. If we drop 'strict-dynamic' and the page only delivers

<meta http-equiv="Content-Security-Policy" content="
  script-src https: 'sha256-GD2MyhNzRFjmxD4jAUnvYIt90C1er46lTotfFdaZ3lg='
">

then chrome loads ./start.js.

I don't know the background, but this makes me wonder whether we should actually consider statically imported scripts as non-parser-inserted, so that 'strict-dynamic' would allow them.

As for <link rel="modulepreload" href="./start.js">, the spec prescribes to fetch this as a non-parser-inserted request with script destination, which is why CSP is happy to allow it because of strict-dynamic. In this case, I am not sure why the html spec would consider this as non-parser-inserted.

@arturjanc
Copy link
Contributor

I actually think his may be a browser bug related to using hashes for module scripts. If you switch the example above to use a nonce (so add nonce-foo to your CSP and have <script nonce=foo type=module>...), the script will execute properly and ./start.js will load and run as well.

AFAIK this is orthogonal to 'strict-dynamic', because dynamic imports don't require using 'strict-dynamic' to load (if a nonced script imports another script, this will be permitted even if the CSP is just script-src 'nonce-foo')

@antosart
Copy link
Member

antosart commented Dec 1, 2022

@arturjanc I think what you are writing confirms my previous comment. As I wrote, the spec prescribes the descendants of a module script to be fetched with the same options as the parent. If the parent has a nonce, the descendants will be fetched with the same nonce stored in the fetch client settings object, and the CSP spec check will succeed because it finds a matching nonce.

Hence I believe the bug is actually in the specification. If we think that static imports should not require anything more to load (i.e. if the parent is allowed to load, then all descendants should automatically be allowed to load), then we should change the spec.

@lmuntaner
Copy link

Thanks for the comments!!

Not that I fully understand it, but it kind of makes sense.

Correct me if I'm wrong:

  • Chrome applies the specifications well, but there might be a bug in the specifications itself.
  • Safari allowing it, it's not following the specifications, nor is Firefox by not allowing any.

Thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants