CVE-2024-23284
Overview
Impact:
Description: Processing maliciously crafted web content may prevent Content Security Policy from being enforced
Researchers: Georg Felber and Marco Squarcina
Key insight. Merging attributes from a duplicate body/html tag re-added the nonce onto an already-connected element without re-hiding it, so a value the spec requires to be invisible to attribute selectors became leakable; hiding (or discarding) the nonce during the merge restores the invariant.
Patch Details
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 ]
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2024-23284 |
| Bugzilla | 267241 |
| Component | WebCore |
| Bug Class | Bypass |
| Severity | medium |
| Commit | 186349180de5ef68… |
| Advisory | Apple Advisory |
Terminology
- 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
1. 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).
2. 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.
3. 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.
4. 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
A CSP nonce disclosure and consequent nonce-CSP bypass in the WebContent process, with no memory corruption. Recovering the nonce lets an attacker who can inject markup escalate to executing scripts a nonce-based CSP should block — turning a markup-injection foothold into an XSS-mitigation bypass. Reach depends on sites using nonce-based CSP, but for those it defeats a primary defense.
Audit Directions
Same function: other hidden attributes
Audit 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 invariants
Grep for hideNonce/[[nonce]]/attributeWithoutSynchronization(nonceAttr) to confirm every path that can set a nonce on a connected element re-runs the hide step.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
HTMLConstructionSite::mergeAttributesFromTokenIntoElement |
Source/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
Source Files
Source/WebCore/html/parser/HTMLConstructionSite.cpp
Test Files
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.js
Regression Test
The fix ships this proof-of-concept / regression test:
diff --git a/LayoutTests/http/wpt/content-security-policy/duplicate-body-hide-nonce-attribute.https-expected.html b/LayoutTests/http/wpt/content-security-policy/duplicate-body-hide-nonce-attribute.https-expected.html
new file mode 100644
index 000000000000..eb8db247e193
--- /dev/null
+++ b/LayoutTests/http/wpt/content-security-policy/duplicate-body-hide-nonce-attribute.https-expected.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<body>
+ <h1>CSP Nonce Bypass</h1>
+ Hello
+</body>
+</html>
diff --git a/LayoutTests/http/wpt/content-security-policy/duplicate-body-hide-nonce-attribute.https.html b/LayoutTests/http/wpt/content-security-policy/duplicate-body-hide-nonce-attribute.https.html
new file mode 100644
index 000000000000..47083668841d
--- /dev/null
+++ b/LayoutTests/http/wpt/content-security-policy/duplicate-body-hide-nonce-attribute.https.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<body>
+ <h1>CSP Nonce Bypass</h1>
+ Hello <style>body[nonce*=secret]{background:red}</style><body
+ <script nonce="secret" src="resources/dummy.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/wpt/content-security-policy/duplicate-body-hide-nonce-attribute.https.html.headers b/LayoutTests/http/wpt/content-security-policy/duplicate-body-hide-nonce-attribute.https.html.headers
new file mode 100644
index 000000000000..67d4c81e589d
--- /dev/null
+++ b/LayoutTests/http/wpt/content-security-policy/duplicate-body-hide-nonce-attribute.https.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: script-src 'nonce-secret'
diff --git a/LayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https-expected.html b/LayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https-expected.html
new file mode 100644
index 000000000000..eb8db247e193
--- /dev/null
+++ b/LayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https-expected.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<body>
+ <h1>CSP Nonce Bypass</h1>
+ Hello
+</body>
+</html>
diff --git a/LayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https.html b/LayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https.html
new file mode 100644
index 000000000000..ae8daf6e70d6
--- /dev/null
+++ b/LayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<body>
+ <h1>CSP Nonce Bypass</h1>
+ Hello <style>html[nonce*=secret]{background:red}</style><html
+ <script nonce="secret" src="resources/dummy.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https.html.headers b/LayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https.html.headers
new file mode 100644
index 000000000000..67d4c81e589d
--- /dev/null
+++ b/LayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: script-src 'nonce-secret'
diff --git a/LayoutTests/http/wpt/content-security-policy/resources/dummy.js b/LayoutTests/http/wpt/content-security-policy/resources/dummy.js
new file mode 100644
index 000000000000..732148a7414e
--- /dev/null
+++ b/LayoutTests/http/wpt/content-security-policy/resources/dummy.js
@@ -0,0 +1 @@
+var x = 2;