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 >
Users experience performance and privacy benefits if the developer can inline
some of those requests instead of asking the user’s agent to make them
directly to the source server. Rather than composing the page at runtime,
develoeprs 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. This can reduce websites' security, increasing
the risk to a site’s users.
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 containing byte sequences
representing base64-decoded signatures, or failure if decoding failed:
-
Let result be an empty list.
-
If el does not have a
signatureattribute, return result. -
For each item resulting from splitting el’s
signatureattribute’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. -
Append the result of forgiving-base64 decoding 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
integrityattribute, return result. -
For each item resulting from splitting el’s
integrityattribute’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:
-
If signature is
failure, continue. -
For each key in keys:
-
Execute the
Ed25519verification 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
signatureattribute represents a set of signatures over the element’s child text content. Thesignatureattribute must not be specified when thesrcattribute is specified.The
signatureIDL attribute reflects the value of thesignaturecontent attribute.
We’ll also remove the restriction against integrity attributes
coexisting with src attributes:
The
integrityattribute represents the integrity metadata for requests which this element is responsible for. The value is text.Theintegrityattribute must not be specified when thesrcattribute 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
integrityattribute represents integrity metadata for the element. The value is text. [SRI].The
signatureattribute represents a set of signatures over the element’s child text content.The
integrityIDL attribute reflects the value of theintegritycontent attribute.
The
signatureIDL attribute reflects the value of thesignaturecontent 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
srccontent 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 thestyleelement, "style", and thestyleelement’s child text content, then return. -
If the
styleelement 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.