CVE-2024-23284
Overview
Background
- CSP nonce
- A per-response random token in script-src ’nonce-…’; only elements carrying the matching nonce may execute, so leaking it defeats the policy.
- Nonce hiding
- Per HTML spec, once connected an element’s nonce is moved to an internal [[nonce]] field and the attribute cleared, so it is not observable via attribute selectors.
- Duplicate body/html attribute merge
- The parser merges attributes from a duplicate body/html tag onto the original element, which could reintroduce a visible nonce attribute.
- CSS attribute-selector exfiltration
- A selector like body[nonce*=secret] plus side channels can read an attribute value the page should not expose to script-less markup.
Root Cause Analysis
This fixes a CSP nonce disclosure (leading to a CSP bypass) caused by attribute merging on duplicate body/html tags. Per the HTML spec, once a nonce-bearing element is connected to the document the user agent must HIDE the nonce: it moves the value into the element’s internal [[nonce]] field and clears the content attribute, so scripts can only read the nonce via the .nonce IDL property and cannot exfiltrate it through side channels such as CSS attribute selectors. The spec also says that when the parser encounters a duplicate body or html tag it merges the duplicate element’s attributes onto the original (already-connected) element. HTMLConstructionSite::mergeAttributesFromTokenIntoElement performed that merge without nonce-specific handling, so it could copy a nonce content attribute from a duplicate body/html onto the original element after the original had already passed its hide-on-connect step — re-exposing the nonce as a live, readable attribute. An attacker who injects markup (but cannot yet run script) can then read the nonce through a CSS attribute selector such as body[nonce*=secret] (using attribute-substring matching plus resource-loading/timing side channels) to recover the per-response CSP nonce, and then inject a script element carrying the stolen nonce — defeating a nonce-based Content Security Policy.
The fix special-cases the nonce attribute during the merge: if the original element’s [[nonce]] internal field is already set it discards the duplicate’s nonce attribute; if the original has no nonce it merges the attribute and immediately runs the hide logic so the nonce is moved to [[nonce]] and cleared.
The restored invariant is that a nonce is never left exposed as a readable attribute on a connected element after attribute merging. The regression tests use duplicate body and html tags with a CSS attribute selector on nonce and a nonce-gated script under a nonce-based CSP.
Attack Path
- Inject duplicate body/html markup On a page with a nonce-based CSP, inject a duplicate body or html tag carrying a nonce attribute (or trigger the merge of one).
- Re-expose the nonce Attribute merging copies the nonce onto the already-connected original element without hiding it, leaving it readable as a live attribute.
- Leak the nonce via CSS Use a CSS attribute-substring selector such as body[nonce*=secret] with resource/timing side channels to recover the nonce character by character.
- Bypass CSP Inject a script element bearing the stolen nonce so it satisfies script-src ’nonce-…’, executing script the CSP was meant to block.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
HTMLConstructionSite::mergeAttributesFromTokenIntoElementSource/WebCore/html/parser/HTMLConstructionSite.cpp |
modified | Special-cases the nonce attribute when merging duplicate body/html attributes: discards it if the original's [[nonce]] is already set, otherwise merges and immediately hides it, so the nonce is never re-exposed as a readable attribute. |
Files Changed
LayoutTests/http/wpt/content-security-policy/duplicate-body-hide-nonce-attribute.https-expected.htmlLayoutTests/http/wpt/content-security-policy/duplicate-body-hide-nonce-attribute.https.htmlLayoutTests/http/wpt/content-security-policy/duplicate-body-hide-nonce-attribute.https.html.headersLayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https-expected.htmlLayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https.htmlLayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https.html.headersLayoutTests/http/wpt/content-security-policy/resources/dummy.jsSource/WebCore/html/parser/HTMLConstructionSite.cpp
Audit Directions
- Same function: other hidden attributesAudit mergeAttributesFromTokenIntoElement and other attribute-copy paths (adoptNode, cloneAttributes, parser fragment merges) for handling of nonce and any attribute that must stay hidden after connection.
- Nonce hide invariantsGrep for hideNonce/[[nonce]]/attributeWithoutSynchronization(nonceAttr) to confirm every path that can set a nonce on a connected element re-runs the hide step.
Patch
diff --git a/LayoutTests/http/tests/security/referrer-policy-header.html b/LayoutTests/http/tests/security/referrer-policy-header.html
index 4bfe3dc27a22..39f7cacc7f70 100644
--- a/LayoutTests/http/tests/security/referrer-policy-header.html
+++ b/LayoutTests/http/tests/security/referrer-policy-header.html
@@ -8,9 +8,10 @@
<script>
description("Tests support for Referrer-Policy HTTP header.");
jsTestIsAsync = true;
-runTests(false);
-if (window.testRunner)
+if (window.testRunner) {
+ testRunner.waitUntilDone();
testRunner.setStatisticsShouldDowngradeReferrer(false, async () => { await runTests(false /* multipart */); });
+}
</script>
</body>
</html>
diff --git a/LayoutTests/platform/wk2/TestExpectations b/LayoutTests/platform/wk2/TestExpectations
index 10c1bdbcf4de..59832160e345 100644
--- a/LayoutTests/platform/wk2/TestExpectations
+++ b/LayoutTests/platform/wk2/TestExpectations
@@ -870,6 +870,4 @@ webkit.org/b/259409 imported/w3c/web-platform-tests/cookies/partitioned-cookies/
webkit.org/b/259482 fast/media/managed-media-source-open-crash.html [ Pass Failure ]
-webkit.org/b/260632 http/tests/security/referrer-policy-header.html [ Pass Failure Crash ]
-
webkit.org/b/260640 [ Release arm64 ] editing/execCommand/apply-inline-style-to-element-with-no-renderer-crash.html [ Pass Failure ]