1. Introduction
Websites are well-understood to be compositions of resources from a variety of
servers, woven together in one origin’s context to satisfy a developer’s goals.
These resources are generally requested through elements like script
and link
, instructing user agents to make explicit requests to other servers
on the page’s behalf, and to include those resources as part of the page’s
construction:
< script src = "https://widgets-r-us.example/widget.js" ></ script >
Developers, however, often have reasons (performance, privacy, etc) to avoid
asking users' agents to take responsibility for these additional requests.
Rather than composing the page at runtime, they might choose to embed code more
directly, copy/pasting snippets into inline script
blocks, or relying upon
layers of infrastructure to inline their dependencies through server-side
includes (Fastly implements a subset of [ESI-LANG], for example). Depending on the development team, these dependencies
might not even be third-party in the traditional sense, but developed as
internally-shared frameworks that are jammed together through internal
infrastructure:
<!-- The main document: --> <esi:include src= "https://widgets-r-us.example/widget.include" />
<!-- https://widgets-r-us.example/widget.include is inlined below --> < script > /* Code goes here. */ </ script > <!-- End of https://widgets-r-us.example/widget.include's content -->
These inlined blocks are a stumbling block for developers who wish to deploy
strong protections against injection attacks, as architectural decisions might
make it difficult to coordinate nonce
attributes or content hashes
between inlined scripts and `Content-Security-Policy
` headers delivered
with the page. Sites might fall back to allowing 'unsafe-inline'
, or simply
forgoing a policy in the first place.
Signatures might provide an option that satisfies developers' need without additionally complicating deployments. In short, if developers can agree with their dependencies on a (set of) signing key(s), they can encode those relatively static constraints in the page’s content security policy, and validate inlined script’s signatures against those known-good keys. This provides a proof of provenance for the code in question, allowing developers to ensure the integrity of their supply chain in a dynamic fashion:
<!-- https://widgets-r-us.example/widget.include --> < script signature = "ed25519-[base64-encoded signature]" integrity = "ed25519-[base64-encoded public key]" > /* Code goes here. */ </ script >
Pages will assert both a signature and a key for a given script
or style
element. Here, we’ll use the test Ed25519 keys from [RFC9421] to demonstrate:
< script signature = "ed25519-hyFFWrQ21vPXZDV07Mn17Q3ufvYBJDs23CeYu1hGUQi4D+LN99D9I1KmXBGV5kBZtf8h4JIxBLoBzIqLdpudDg==" integrity = "ed25519-JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=" > alert( 1 ); </ script >
Pages can restrict execution of script through reference to these keys:
Content-Security-Policy: script-src 'ed25519-JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs='
|
2. Framework
2.1. Validation
To parse signatures given an element el, execute the following steps, which return a list of byte sequences:
-
Let result be an empty list.
-
If el does not have a
signature
attribute, return result. -
For each item resulting from splitting el’s
signature
attribute’s value on ASCII whitespace:-
Let algorithm-and-value be the result of splitting item on U+002D (
-
). -
If algorithm-and-value[0] is not "
ed25519
", continue. -
Let decoded be the result of forgiving-base64 decoding algorithm-and-value[1].
-
If decoded is failure, continue.
-
Append algorithm-and-value[1] to result.
-
-
Return result.
To parse keys given an element el, execute the following steps, which return a list of byte sequences:
-
Let result be an empty list.
-
If el does not have an
integrity
attribute, return result. -
For each item resulting from splitting el’s
integrity
attribute’s value on ASCII whitespace:-
Let algorithm-and-value be the result of splitting item on U+002D (
-
). -
If algorithm-and-value[0] is not "
ed25519
", continue. -
Let decoded be the result of forgiving-base64 decoding algorithm-and-value[1].
-
If decoded is failure, continue.
-
Append algorithm-and-value[1] to result.
-
-
Return result.
Note: This is partially a simplification of SRI’s parse metadata algorithm. It might be reasonable to rely on that directly in the future.
An element has an invalid inline signature if the
following algorithm returns "Invalid
" given an element el:
-
Let signatures be the result of executing parse signature on el.
-
If signatures is empty, return "
Valid
". -
Let keys be the result of executing parse keys on el.
-
Let inline content be el’s child text content.
-
For each signature in signatures:
-
For each key in keys:
-
Execute the
Ed25519
verification algorithm as defined in Section 5.1.7 of [RFC8032] using key as the public key portion of the verification key material (A
), inline content as the message (M
), and signature as the signature to be verified. -
If verification succeeded, return "
Valid
". Otherwise continue.
-
Note: This means that the algorithm will return "
Valid
" if any asserted signature matches any specified key. -
-
Return "
Invalid
".Note: If no keys are specified, no signatures can be validated. We could punt on this condition after step 3, but it feels reasonable to fail closed if a signature was asserted but couldn’t be validated.
2.2. Monkey-patching HTML
The following changes wire up both script
and style
elements to perform
signature validation before being used on a page:
2.2.1. <script>
Patches #
The following additions to script
are required:
partial interface HTMLScriptElement { [CEReactions ]attribute DOMString ; };
signature The
signature
attribute represents a set of signatures over the element’s child text content. Thesignature
attribute must not be specified when thesrc
attribute is specified.The
signature
IDL attribute reflects the value of thesignature
content attribute.
We’ll also remove the restriction against integrity
attributes
coexisting with src
attributes:
The
integrity
attribute represents the integrity metadata for requests which this element is responsible for. The value is text.Theintegrity
attribute must not be specified when thesrc
attribute is not specified.
We could also reconsider the restrictions on using integrity
on data blocks and import maps. Both could benefit from this kind of
integrity check?
2.2.2. <style>
Patches #
The following additions to style
are necessary:
partial interface HTMLStyleElement { [CEReactions ]attribute DOMString ; [
integrity CEReactions ]attribute DOMString ; };
signature The
integrity
attribute represents integrity metadata for the element. The value is text. [SRI].The
signature
attribute represents a set of signatures over the element’s child text content.The
integrity
IDL attribute reflects the value of theintegrity
content attribute.
The
signature
IDL attribute reflects the value of thesignature
content attribute.
2.2.3. Script execution
We’ll add a new validation step alongside the callout to CSP in step 19 of HTML’s prepare the script element algorithm as follows:
-
If el does not have a
src
content attribute, then return if either of the following statements is true:-
The Should element’s inline type behavior be blocked by Content Security Policy? algorithm returns "
Blocked
" when given el, "script
", and source text.
-
2.2.4. Style application
We’ll add a new validation step alongside the callout to CSP in step 5 of HTML’s update a style block algorithm as follows:
-
If the Should element’s inline type behavior be blocked by Content Security Policy? algorithm returns "
Blocked
" when given thestyle
element, "style
", and thestyle
element’s child text content, then return. -
If the
style
element has an invalid inline signature, then return.
3. Implementation Considerations
3.1. What is being signed?
The signature is asserted over the script
or style
element’s child text content, which importantly includes both leading and trailing
whitespace. That means that <script>alert(1);</script>
will have a different
signature than <script> alert(1);</script>
and <script>alert(1); </script>
.
3.2. How does this compare to Signature-Based Integrity?
The signature-based integrity proposal relies upon HTTP Message Signatures [RFC9421] to explain how signatures can be validated over resources requested from a remote server. As the request and response metadata plays an important role in how the resource is treated by the user agent, the intermediate signature base concept is a necessary complexity that allows a server to ensure that the signature covers all the relevant data.
Here, we have a simpler task: the entirety of the content to be validated is embedded in the document, available at parse time. We can work with the content directly, as there’s no relevant metadata.
This means that a resource delivered via HTTP will have a different signature than a resource delivered inline, even if the keys used are the same. This is unfortunate, but the alternative of synthesizing a signature base for inline content seems worse in practically every way.
4. Security Considerations
4.1. Integration with CSP
Broadly, the mechanism described here aims to make it easier for developers to
deploy protections against unintended injection attacks even while relying
upon inlined script
or style
blocks. It aims to do so in a way
consistent with existing protections like [SRI] and [CSP], giving developers
a clear path towards more safely including their dependencies.
Rather than allowing 'unsafe-inline'
, developers will have the option of
restricting themselves to inline scripts signed by a specific key.
5. Privacy Considerations
5.1. Implications for Content Blocking
Developers inline script in many cases to improve user experience, but script is also inlined for purposes that undercut user agency. It’s more difficult, for example, for extensions and other mediating software to modify or block particular resources when they’re not fetched independently, but are instead part of the document itself.
This proposal has the potential to remove some of the security risk associated with inlined script, but might also be seen as encouraging inlining in ways that could have negative privacy implications. Two considerations mitigate this risk:
-
This proposal doesn’t create any more encouragement to inline script for the purposes of evading user’s intent to block it than the status quo already does. It allows developers to remove one risk associated with inlined content, but that seems quite unlikely to shift incentives to anything near the extent that content-blocking extensions already do.
-
Tying inline content to a specific public key to prove provenance might provide an additional hook for content blocking scripts that could allow more clean identification of a given script’s owner. As these keys are more static than the content itself, this proposal might actually simplify the process of pointing to a specific script in a document as being worthy of additional inspection.