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

Inline scripts, CSP, and SRI #10

Open
devd opened this issue Nov 29, 2017 · 17 comments
Open

Inline scripts, CSP, and SRI #10

devd opened this issue Nov 29, 2017 · 17 comments

Comments

@devd
Copy link

devd commented Nov 29, 2017

If CSP whitelists a hash, an inline script with that hash or a remote script with that hash in its integrity attributes are both signed. If a CSP whitelists a public key, can we figure out a way to get it to work with inline scripts? Can we reuse the integrity attribute somehow? Or do we need a new attribute?

@mikewest
Copy link
Member

mikewest commented Oct 9, 2024

I'd continue punting on this for the moment. Let's work out how we do things in HTTP, and then determine how to apply that to HTML.

Guessing wildly, if we end up running with the model in #16, we'd add some attributes to a script block that allowed the expression of a signature over that block, and keep the key in the integrity attribute as today? Maybe? Later. :)

@yoavweiss
Copy link
Collaborator

I agree with the "later" sentiment. It'd be great to see actual deployment use cases before investing efforts in this direction.

@bakkot
Copy link
Contributor

bakkot commented Jan 14, 2025

I have an actual deployment use case which basically requires this, which I believe to be very common in industry: I am creating a page (or in some cases a portion of a page) with a dynamically generated inline script, and the CSP is added to that page downstream from me - significantly downstream from me, often by a different company entirely and not just a different part of my org.

The script really needs to be inline for performance and operational reasons, so I can't use a host source. The script is dynamic so I can't use a hash. There is no way easy way to have out-of-band communication between me and the thing which is adding the CSP, so I can't use a nonce. That means I am forced to tell the people maintaining the downstream server that they must add 'unsafe-inline' to the CSP for this page; there is no alternative. That's what we currently do, and this, by itself, is responsible for a significant fraction of the 'unsafe-inline' CSPs in the wild.

If I could use a public key for inline elements instead, all would be well: when generating the page I'd sign the script's contents and add integrity and signature attributes to the inline script element, and I'd tell the downstream server to add the (fixed) public key to their CSP. Now there's no further coordination required.

Please don't neglect this use case.

@mikewest
Copy link
Member

Thanks for the use case, @bakkot! I still see this as an enhancement that isn't necessary for getting something out the door, but I do think we have a reasonably stable HTTP side of things at the moment that should make it possible to sketch this in more detail.

I think it boils down to two things:

  1. Teaching SRI to apply integrity attributes' metadata to the content of a given script block. I recall there being some questions around encodings here (that @annevk might remember? I didn't find them in a quick skim through Apply integrity checks to inline script and style blocks. w3c/webappsec-subresource-integrity#86). We poked at this a bit a while ago, but there's not a super-compelling use case for inline digests, and we never got it over the line. I think there's more justification for signatures, even if only in split-responsibility cases like the one you've described here.

  2. Teaching HTML to store a signature in a way that allows us to validate content. Since it's possible for more than one signature to be available (for key rotation purposes if nothing else), there's potentially a little complexity about mapping the integrity metadata to the signatures defined in a signature attribute, but I'm sure we could work it out. On the HTTP side we bind the key and signature together via a label in the structured header dictionary. On the HTML side, we could either do something similar (by adding a label to the public key in the integrity attribute, e.g. ed25519-...?label=x), or by just trying to validate all asserted signatures with all declared keys, and declaring success if there's any match. That's not crazy, though the mismatch between matching "all" on the HTTP side vs "any" on the HTML side is something I'd want to think about a bit before committing to it.

WDYT?

@bakkot
Copy link
Contributor

bakkot commented Jan 15, 2025

I agree it's not strictly necessary for getting something out the door, though given how often things seem to get partially done and then never fully updated to address other identified use cases I'd really like to push for including this in the initial spec rather than punting to the indefinite future and continuing to tell people they have to use unsafe-inline.

Teaching SRI to apply integrity attributes' metadata to the content of a given script block

Well, yes if we're overloading that attribute, though that's less of an issue if using a different attribute, in which case it would only need to be updated to check signatures and not hashes.

Teaching HTML to store a signature in a way that allows us to validate content. [...] On the HTML side, we could either do something similar (by adding a label to the public key in the integrity attribute, e.g. ed25519-...?label=x), or by just trying to validate all asserted signatures with all declared keys, and declaring success if there's any match.

Either of those seem fine. Though the simplest thing would be to just stuff the key into the signature attribute for inline scripts, as in signature="ed25519-whatever:base64-of-signature". In that case I suppose we wouldn't even need to support integrity or integrity-key on inline scripts, just signature.


Also, CSP will need to be updated as well so that it knows to allow inline scripts whose integrity declares a key which is present in the hash-sources list.

It's unfortunate that there's currently this split responsibility for checking hashes, where SRI does external ones and CSP does inline ones, and this would further complicate things in that SRI will be responsible for checking signatures for both external and internal scripts. But that's just complexity on the spec side, not something which affects developers.

@bakkot
Copy link
Contributor

bakkot commented Jan 15, 2025

I should mention that there's use cases for a signature attribute on external scripts too: this would allow you to load a script from a source where you're not in a position to add headers, like a CDN, which is obviously something one very often wants to do.

@mikewest
Copy link
Member

I agree it's not strictly necessary for getting something out the door, though given how often things seem to get partially done and then never fully updated to address other identified use cases I'd really like to push for including this in the initial spec rather than punting to the indefinite future and continuing to tell people they have to use unsafe-inline.

The bulk of the interest I've seen thus far has been from folks operating servers which deliver script into someone else's page via <script src>, and that satisfying that use case is a thing in itself that doesn't require also satisfying use cases around provenance checks for inline scripts. You're presenting a use case I haven't thought a lot about ("third-party" inline script), which is somewhat outside the threat model I'd considered.

If folks are willing to spend time on it, I certainly don't object to getting it done now as opposed to later. To that end, it'd be helpful to have a broader set of folks who want inline signature checks to make sure that we understand the use cases and can satisfy their requirements, and to help browser vendors understand the relative priority of this work vs everything else on their plates.

Teaching SRI to apply integrity attributes' metadata to the content of a given script block

Well, yes if we're overloading that attribute, though that's less of an issue if using a different attribute, in which case it would only need to be updated to check signatures and not hashes.

Yup. I think the work (both in specs and implementations) would be the ~same in either case, since validating the signature requires generating a content digest.

Teaching HTML to store a signature in a way that allows us to validate content. [...] On the HTML side, we could either do something similar (by adding a label to the public key in the integrity attribute, e.g. ed25519-...?label=x), or by just trying to validate all asserted signatures with all declared keys, and declaring success if there's any match.

Either of those seem fine. Though the simplest thing would be to just stuff the key into the signature attribute for inline scripts, as in signature="ed25519-whatever:base64-of-signature". In that case I suppose we wouldn't even need to support integrity or integrity-key on inline scripts, just signature.

Right. You could imagine a few ways of spelling this.

That said, another question this raises is how well the HTTP Message Signature framework that the HTTP side of this proposal depends upon applies to inline content. In particular, what are we actually signing for inline scripts? I think we have two broad options:

  1. We synthesize the header structure that would have supported a signature across the inline script, generate a signature base in exactly the same way as we would have for an HTTP response, and sign across that.

  2. We just sign the script content directly.
    Neither seems particularly easy to explain (the former seems like more work than it should be, the latter raises questions about why the inline signature diverges from the HTTP signature).

Also, CSP will need to be updated as well so that it knows to allow inline scripts whose integrity declares a key which is present in the hash-sources list.

Yup. This needs to happen in any event (#36).

I should mention that there's use cases for a signature attribute on external scripts too: this would allow you to load a script from a source where you're not in a position to add headers, like a CDN, which is obviously something one very often wants to do.

I understand the use case, and it's not unreasonable. We'd need to specify not only the signature but also the metadata over which the signature is generated (basically replicating the signature-input and signature header content in the HTML element) so that we could construct the right signature base over the right set of components (or, I suppose, sign over the content as per 2 above, though that seems even more confusing to me than doing so for inline content).

The content digest needs to be part of that base in order to tie it unambiguously to the response it purports to validate. If you need to provide the digest along with the input and signature, it seems like you're losing the deployment advantage of signatures.

@bakkot
Copy link
Contributor

bakkot commented Jan 15, 2025

To that end, it'd be helpful to have a broader set of folks who want inline signature checks to make sure that we understand the use cases and can satisfy their requirements, and to help browser vendors understand the relative priority of this work vs everything else on their plates.

I can try to get other people to chime in, though the sort of people who run into this kind of thing do not tend to be the sort of people who are working on web specs.

That said, you can do some of this research for yourself: pick a random retailer - Apple, Walmart, CVS, Aldi, whatever - and view-source, and you will almost certainly find inline scripts. And if they have a CSP at all, they are almost certainly using 'unsafe-inline' for those scripts, rather than nonces (or hashes). I can tell you, from experience, that this is because the teams managing those scripts are not the same as the teams managing the CSP header (possibly not even at the same company), and that the architecture of the sites makes it impractical to coordinate the CSP header and the script elements on the page to ensure that they have matching nonces or hashes on each request: in other words, exactly the same problem you have with external scripts. And the fix is the same: you want to have fixed component in the CSP header (which is more restrictive than 'unsafe-inline'), which allows the people managing or generating the scripts to update their script without needing to coordinate changes to the header.

In particular, what are we actually signing for inline scripts?

I haven't actually read RFC 9421, but the readme has this: Signature: sig1=:[base64-encoded result of Ed25519(`console.log("Hello, world!");`, [private key])]:

That implied to me that for HTTP the expectation is that we're signing exactly the content of the script, at least in the default case. So I was assuming we'd be doing the same here. Is that not so?

If you need to provide the digest along with the input and signature, it seems like you're losing the deployment advantage of signatures.

Say more about this? Isn't the digest just a function of the contents? Why would you need to provide it? Also, Ed25519 already computes a digest as part of its operation.

@mikewest
Copy link
Member

To that end, it'd be helpful to have a broader set of folks who want inline signature checks to make sure that we understand the use cases and can satisfy their requirements, and to help browser vendors understand the relative priority of this work vs everything else on their plates.

I can try to get other people to chime in, though the sort of people who run into this kind of thing do not tend to be the sort of people who are working on web specs.

That said, you can do some of this research for yourself: pick a random retailer - Apple, Walmart, CVS, Aldi, whatever - and view-source, and you will almost certainly find inline scripts. And if they have a CSP at all, they are almost certainly using 'unsafe-inline' for those scripts, rather than nonces (or hashes). I can tell you, from experience, that this is because the teams managing those scripts are not the same as the teams managing the CSP header (possibly not even at the same company), and that the architecture of the sites makes it impractical to coordinate the CSP header and the script elements on the page to ensure that they have matching nonces or hashes on each request: in other words, exactly the same problem you have with external scripts. And the fix is the same: you want to have fixed component in the CSP header (which is more restrictive than 'unsafe-inline'), which allows the people managing or generating the scripts to update their script without needing to coordinate changes to the header.

Useful feedback, thanks.

It would be helpful to understand more about the ways in which these externally-controlled inline scripts are included, which I imagine varies from folks copy/pasting the scripts into their CRM on the one end, through to server-side includes or more complicated distribution methods on the other. The latter would seem conducive to passing nonces, while the former certainly requires stasis.

Both approaches do require some level of coordination between the CSP header (even if delivered as a <meta> tag inline) and the script, though: key rotation is going to be necessary at some point, even in the best case scenarios. Considering those entirely separable seems difficult.

In particular, what are we actually signing for inline scripts?

I haven't actually read RFC 9421, but the readme has this: Signature: sig1=:[base64-encoded result of Ed25519(`console.log("Hello, world!");`, [private key])]:

That implied to me that for HTTP the expectation is that we're signing exactly the content of the script, at least in the default case. So I was assuming we'd be doing the same here. Is that not so?

That is indeed an oversimplification: https://wicg.github.io/signature-based-sri/#basic-example and https://wicg.github.io/signature-based-sri/#examples tell a more fulsome story.

TL;DR: We sign something we can verify as soon as we have the HTTP Message Signature headers, which RFC9421 refers to as a "signature base". That contains the digest of the content, which we then independently verify against the content once it finishes streaming in. That is, the signature is over something like:

"identity-digest";sf: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
"@signature-params": ("identity-digest";sf);keyid="JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";tag="sri"

Not:

{"hello": "world"}

If you need to provide the digest along with the input and signature, it seems like you're losing the deployment advantage of signatures.

Say more about this? Isn't the digest just a function of the contents? Why would you need to provide it? Also, Ed25519 already computes a digest as part of its operation.

I guess you're right that we could synthesize the digest along with the rest of the signature base. That has some substantial implications for the implementation (I'd need to move all Chromium's validation to the renderer and out of the network stack, which has implications I haven't thought through), but it's certainly possible.

@bakkot
Copy link
Contributor

bakkot commented Jan 15, 2025

It would be helpful to understand more about the ways in which these externally-controlled inline scripts are included, which I imagine varies from folks copy/pasting the scripts into their CRM on the one end, through to server-side includes or more complicated distribution methods on the other. The latter would seem conducive to passing nonces, while the former certainly requires stasis.

Something like server-side includes is the common case, yeah.

But I don't know why you think that's conducive to passing nonces. The (often several) servers (or build systems) which are adding in the scripts are usually well upstream of, and in some cases completely oblivious to, the servers which are adding in the headers, and have no particular way to communicate with them to coordinate on a nonce. Here you should imagine the CSP header being inserted by something like BIG-IP's Request Header Insert or Cloudflare's HTTP response header modification rules. These rules are managed by humans and can be manually updated for rare events like key rotation, but you wouldn't want a human to have to manually update them every time one of the scripts on the page changed its contents (especially when the scripts are dynamically generated).

Furthermore, the HTML is generally cached after being generated (but before adding the headers), and rewriting it on every request to add nonces would be prohibitively expensive even if it were technically feasible to coordinate with the downstream server which is adding the CSP header.

Note, also, that I'm not just talking about externally-controlled scripts. That's the case I personally have to deal with, but it is at least as common that the script is produced internally but by a completely different team (and server) than the team (and server) which is managing the CSP header, such that coordinating across those servers would require redoing the entire network stack.

That is indeed an oversimplification

Ah. Alas. That's a lot of machinery which doesn't seem very necessary here.

I'd advocate for the thing signed for inline signatures to just be the contents of the script, rather than trying to pull in the rest of the complicated machinery.

@bakkot
Copy link
Contributor

bakkot commented Jan 15, 2025

Thinking further, a key plus a signature over the contents is actually a lot more like the existing hashes in the integrity attribute than just a key alone. Indeed, a key plus a signature is exactly like the existing hashes in that it guarantees the exact content of the script and can be checked by looking at that value plus contents of the script. It has the additional property that it provides identity as well but from the point of view of the spec and implementations it's identical.

So a concrete proposal, pulling in this issue plus #42:

  • take header-based authentication from this proposal out of the integrity attribute and move it to a new identity attribute, which applies only to external scripts; the rest of that machinery stays the same.
  • update the existing integrity attribute to support values of the form ed25519-[base64-encoded public key]:[base64-encoded signature of contents], and re-use the existing SRI machinery (in the specs and in implementations) for validating signatures
  • support integrity on inline scripts

The values in CSP would still just look like ed25519-[base64-encoded public key]. An script would be allowed to load if it had either an integrity where that was the first half of the value or (for external scripts) an identity containing that exact value.

@mikewest
Copy link
Member

But I don't know why you think that's conducive to passing nonces.

Nonces could be passed in along with the request for the script. This assumes more capacity for dynamic content than you suggest is available, though, which is unfortunate.

The (often several) servers (or build systems) which are adding in the scripts are usually well upstream of, and in some cases completely oblivious to, the servers which are adding in the headers, and have no particular way to communicate with them to coordinate on a nonce.

That sounds complex indeed.

I'd advocate for the thing signed for inline signatures to just be the contents of the script, rather than trying to pull in the rest of the complicated machinery.

That's a reasonable thing to advocate for. I think it creates some confusion when an inline script has an entirely different signature than one loaded over HTTP, but perhaps that's the right tradeoff (as we'll discuss below).


Thinking further, a key plus a signature over the contents is actually a lot more like the existing hashes in the integrity attribute than just a key alone. Indeed, a key plus a signature is exactly like the existing hashes in that it guarantees the exact content of the script and can be checked by looking at that value plus contents of the script. It has the additional property that it provides identity as well but from the point of view of the spec and implementations it's identical.

So a concrete proposal, pulling in this issue plus #42:

  • take header-based authentication from this proposal out of the integrity attribute and move it to a new identity attribute, which applies only to external scripts; the rest of that machinery stays the same.
  • update the existing integrity attribute to support values of the form ed25519-[base64-encoded public key]:[base64-encoded signature of contents], and re-use the existing SRI machinery (in the specs and in implementations) for validating signatures
  • support integrity on inline scripts

I appreciate the insight here that it might be reasonable to treat inline content as distinct from content requested from the network. Let me push that a bit further, as I think there's value in creating some mechanism that allows developers to make provable assertions about content inlined directly, but I don't think the constraints there are actually similar to the draft spec we're discussing, and separating them clearly makes it easier to explain why the signed content is different in each case. So, rather than trying to jam them both into the same underlying infrastructure, let's consider the one thing to be relevant to subresources loaded from elsewhere, and the other thing to be relevant to an element's content.

My version of this distinction basically inverts yours. :) I'd suggest that we continue treating integrity as validating the content and/or provenance of subresources loaded via HTTP. We'll continue verifying digests as we do today, and tack on signatures as described in the doc we're discussing.

For a page's content (including but not limited to scripts), I don't think there's ~any value in embedding digests, but signatures seem more reasonable to support. Rather than relying on HTTP Message Signatures for a page's inline content, we could allow developers to assert a set of trusted keys for a given document (e.g. through an inline-keys header or <meta name="inline-keys" content="..."> element or etc.), and to annotate elements on their page (e.g. as <element inline-signature="...">) whose content could be validated/rejected at parse(?) time. I'd have to dig through HTML to recall anything useful about parsing/serialization to make a real proposal about how that would work, but it seems plausible, and would be a more foundational primitive that I could imagine being useful as a way of constraining the kinds of composed-from-multiple-authors pages that you're pointing to above.

(As an aside, this discussion vaguely reminds me of @arturjanc's proposal to extend hashes to cover URLs declared through <script src>, which could perhaps have improved protection against injection even if we allowed inline declaration of the metadata by relying upon a key and signature in the page rather than a hash? Worth considering.)

@mikewest
Copy link
Member

(@annevk might know if anyone has suggested this kind of validation for HTML content in the past (and might have opinions about it, in any of its variously possible spellings))

@annevk
Copy link
Contributor

annevk commented Jan 16, 2025

Let's see:

  • What contents to validate can probably follow the definition of the script element's text getter. But yeah, I guess you would have to UTF-8 encode those(?), which depending on how dynamic all this is might give you issues with unpaired surrogates.
  • This reminds me a bit of base64 entities: https://lists.w3.org/Archives/Public/public-whatwg-archive/2010Aug/thread.html#msg675. (I think there were some other proposals around this time too, but I forgot their names.)
  • It also reminds me of the nonce attribute.
  • I think <script signature> we could do, but I'm less sure about arbitrary elements. That would require end tag notifications for all elements which reportedly is expensive in at least some implementations. I suppose it could be optimized but still. For arbitrary elements it's also much less clear what the contents would be that you are validating.

@mikewest
Copy link
Member

Let's see:

  • What contents to validate can probably follow the definition of the script element's text getter. But yeah, I guess you would have to UTF-8 encode those(?), which depending on how dynamic all this is might give you issues with unpaired surrogates.

I guess we'd do the same thing here that we do for CSP (which consumes the element's child text content via step 19 of prepare the script element.

That proposal seems more direct than this one, insofar as the content is referenced by its exact base64 encoding rather than a proxy (digest or signature), but I see the similarity.

  • It also reminds me of the nonce attribute.

Possibly? If we wanted to extend the concept, we could squint and call this a content nonce (e.g. a number used once for a given paring of content and key?), and just assign special meaning to a nonce that began with a given signature algorithm prefix (e.g. script-src 'nonce-ed25519-...' & <script nonce='ed25519-...'>).

  • I think <script signature> we could do, but I'm less sure about arbitrary elements. That would require end tag notifications for all elements which reportedly is expensive in at least some implementations. I suppose it could be optimized but still. For arbitrary elements it's also much less clear what the contents would be that you are validating.

I'm not entirely sure about the value of extending this to arbitrary elements: scripts and stylesheets will likely remain the most meaningful content to protect, and if it's expensive to do the same for other elements, it seems fine to exclude them.

That said, if you really are embedding arbitrary content from a partner, it would be ideal to ensure that certain important content renders only if it's signed by that partner. Terms of service for a sweepstakes that some startup is running on your behalf? 🤷

Interesting thought experiment in any event.

@annevk
Copy link
Contributor

annevk commented Jan 16, 2025

<iframe srcdoc> would be as hard as <script> I suspect. If we end up revisiting client-side HTML includes that could also lead to a similar feature as srcdoc (inline client-side HTML includes) which could then be validated I suppose. Though all that is much further away, if it ever happens. whatwg/html#2791

@mikewest
Copy link
Member

In an effort to avoid some more important work (and the news), I sketched something out in https://mikewest.github.io/inline-integrity/ that only focuses on <script> and <style>. If there's a use case for a broader set of elements, we can evaluate the expense. 🤷 What do y'all think?

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

No branches or pull requests

5 participants