Released March 5, 2024

CVE-2024-23284

CVSS 6.5 severity medium CWE-693 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N

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.html
  • LayoutTests/http/wpt/content-security-policy/duplicate-body-hide-nonce-attribute.https.html
  • LayoutTests/http/wpt/content-security-policy/duplicate-body-hide-nonce-attribute.https.html.headers
  • LayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https-expected.html
  • LayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https.html
  • LayoutTests/http/wpt/content-security-policy/duplicate-html-hide-nonce-attribute.https.html.headers
  • LayoutTests/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;