Chrome · Parser
CVE-2026-17943
Logic Error in Parser
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/frame/csp/content_security_policy.cc |
modified | |
forthird_party/blink/renderer/core/html/parser/html_preload_scanner.cc |
modified | |
TEST_Fthird_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc |
modified | |
forthird_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/frame/csp/content_security_policy.ccthird_party/blink/renderer/core/frame/csp/content_security_policy.hthird_party/blink/renderer/core/html/parser/html_preload_scanner.ccthird_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc
Patch
From 53096e81cb3220c0a1ebf0a4b0115a1a4207ca10 Mon Sep 17 00:00:00 2001 From: Leo Lee <[email protected]> Date: Mon, 22 Jun 2026 11:06:54 -0700 Subject: [PATCH] Mirror CSP dangling-markup nonce mitigation in preload scanner The preload scanner could speculatively authorize fetches using a nonce that ContentSecurityPolicy::IsNonceableElement would later reject due to dangling-markup-injection signals (duplicate attributes, or attribute names/values containing <SCRIPT/<STYLE/<LINK). This mismatch allowed an attacker to trigger speculative network requests that bypass CSP nonce protections. Mirror the same dangling-markup checks in StartTagScanner::ProcessAttributes: if duplicate attribute names or tag-like substrings are detected, clear the nonce so the speculative preload is blocked. Also adds unit tests covering dangling markup in attribute values, attribute names, and duplicate attributes. Bug: 514424283 Change-Id: Icfc327cd653420b245369c25d3a7d09e81ab56a1 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7879780 Reviewed-by: Dan Clark <[email protected]> Commit-Queue: Leo Lee <[email protected]> Reviewed-by: Jacques Newman <[email protected]> Cr-Commit-Position: refs/heads/main@{#1650441} --- diff --git a/third_party/blink/renderer/core/frame/csp/content_security_policy.cc b/third_party/blink/renderer/core/frame/csp/content_security_policy.cc index 181462e9..83ecb4fb 100644 --- a/third_party/blink/renderer/core/frame/csp/content_security_policy.cc +++ b/third_party/blink/renderer/core/frame/csp/content_security_policy.cc @@ -220,6 +220,21 @@ return CSPStripURL(url).GetString(); } +// static +bool ContentSecurityPolicy::ContainsDanglingMarkupSignal( + const String& attribute_name, + const String& attribute_value) { + static const char kScriptString[] = "<SCRIPT"; + static const char kStyleString[] = "<STYLE"; + static const char kLinkString[] = "<LINK"; + return attribute_name.FindIgnoringAsciiCase(kScriptString) != kNotFound || + attribute_name.FindIgnoringAsciiCase(kStyleString) != kNotFound || + attribute_name.FindIgnoringAsciiCase(kLinkString) != kNotFound || + attribute_value.FindIgnoringAsciiCase(kScriptString) != kNotFound || + attribute_value.FindIgnoringAsciiCase(kStyleString) != kNotFound || + attribute_value.FindIgnoringAsciiCase(kLinkString) != kNotFound; +} + bool ContentSecurityPolicy::IsNonceableElement(const Element* element) { if (element->nonce().IsNull()) return false; @@ -238,18 +253,8 @@ nonceable = false; if (nonceable) { - static const char kScriptString[] = "<SCRIPT"; - static const char kStyleString[] = "<STYLE"; - static const char kLinkString[] = "<LINK"; for (const Attribute& attr : element->Attributes()) { - const AtomicString& name = attr.LocalName(); - const AtomicString& value = attr.Value(); - if (name.FindIgnoringAsciiCase(kScriptString) != kNotFound || - name.FindIgnoringAsciiCase(kStyleString) != kNotFound || - name.FindIgnoringAsciiCase(kLinkString) != kNotFound || - value.FindIgnoringAsciiCase(kScriptString) != kNotFound || - value.FindIgnoringAsciiCase(kStyleString) != kNotFound || - value.FindIgnoringAsciiCase(kLinkString) != kNotFound) { + if (ContainsDanglingMarkupSignal(attr.LocalName(), attr.Value())) { nonceable = false; break; } diff --git a/third_party/blink/renderer/core/frame/csp/content_security_policy.h b/third_party/blink/renderer/core/frame/csp/content_security_policy.h index a7c39c8..984c032 100644 --- a/third_party/blink/renderer/core/frame/csp/content_security_policy.h +++ b/third_party/blink/renderer/core/frame/csp/content_security_policy.h @@ -419,6 +419,12 @@ static bool IsNonceableElement(const Element*); + // Returns true if the attribute name or value contains a dangling markup + // signal ("<SCRIPT", "<STYLE", or "<LINK"), indicating a potential nonce + // hijacking attempt. + static bool ContainsDanglingMarkupSignal(const String& attribute_name, + const String& attribute_value); + static const char* GetDirectiveName(CSPDirectiveName type); static CSPDirectiveName GetDirectiveType(const String& name); diff --git a/third_party/blink/renderer/core/html/parser/html_preload_scanner.cc b/third_party/blink/renderer/core/html/parser/html_preload_scanner.cc index 88c7e7d..7a8389ca 100644 --- a/third_party/blink/renderer/core/html/parser/html_preload_scanner.cc +++ b/third_party/blink/renderer/core/html/parser/html_preload_scanner.cc @@ -222,9 +222,32 @@ String attribute_value = html_token_attribute.Value(); ProcessAttribute(attribute_name, attribute_value); } + MaybeClearNonceForDanglingMarkup(attributes); PostProcessAfterAttributes(); } + // Mirror the dangling-markup-injection mitigation in + // ContentSecurityPolicy::IsNonceableElement so the preload scanner cannot + // be tricked into authorizing a speculative fetch with a hijacked nonce. + // This is done as a separate pass after ProcessAttributes so we can skip + // the work entirely when no nonce is present. + void MaybeClearNonceForDanglingMarkup( + const HTMLToken::AttributeList& attributes) { + if (nonce_.IsNull() || nonce_.empty()) + return; + HashSet<AtomicString> seen_names; + for (const HTMLToken::Attribute& html_token_attribute : attributes) { + AtomicString attribute_name(html_token_attribute.GetName()); + String attribute_value = html_token_attribute.Value(); + if (!seen_names.insert(attribute_name).is_new_entry || + ContentSecurityPolicy::ContainsDanglingMarkupSignal( + attribute_name, attribute_value)) { + SetNonce(String()); + return; + } + } + } + void PostProcessAfterAttributes() { if (Match(tag_impl_, html_names::kImgTag) || (link_is_preload_ && as_attribute_value_ == "image")) diff --git a/third_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc b/third_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc index 5dc7b2b..d48b7f5 100644 --- a/third_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc +++ b/third_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc @@ -1305,6 +1305,35 @@ } } +TEST_F(HTMLPreloadScannerTest, testNonceDanglingMarkup) { + NonceTestCase test_cases[] = { + // Dangling markup in attribute value should strip nonce. + {"http://example.test", + "<script src='/script' nonce='abc' foo='x<script'></script>", ""}, + {"http://example.test", + "<script src='/script' nonce='abc' foo='x<style'></script>", ""}, + {"http://example.test", + "<script src='/script' nonce='abc' foo='x<link'></script>", ""}, + // Dangling markup in attribute name should strip nonce. + {"http://example.test", + "<script src='/script' nonce='abc' x<script='foo'></script>", ""}, + // Duplicate attributes should strip nonce. + {"http://example.test", + "<script src='/script' nonce='abc' foo='a' foo='b'></script>", ""}, + // Normal case: nonce preserved when no dangling markup signals. + {"http://example.test", "<script src='/script' nonce='abc'></script>", + "abc"}, + // Link with dangling markup should also be stripped. + {"http://example.test", + "<link rel='stylesheet' href='/style' nonce='abc' bar='<link'>", ""}, + }; + + for (const auto& test_case : test_cases) { + SCOPED_TRACE(test_case.input_html); + Test(test_case); + } +} + TEST_F(HTMLPreloadScannerTest, testAttributionSrc) { static constexpr bool kSecureDocumentUrl = true; static constexpr bool kInsecureDocumentUrl = false;
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc b/third_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc
index 5dc7b2b..d48b7f5 100644
--- a/third_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc
+++ b/third_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc
@@ -1305,6 +1305,35 @@
}
}
+TEST_F(HTMLPreloadScannerTest, testNonceDanglingMarkup) {
+ NonceTestCase test_cases[] = {
+ // Dangling markup in attribute value should strip nonce.
+ {"http://example.test",
+ "<script src='/script' nonce='abc' foo='x<script'></script>", ""},
+ {"http://example.test",
+ "<script src='/script' nonce='abc' foo='x<style'></script>", ""},
+ {"http://example.test",
+ "<script src='/script' nonce='abc' foo='x<link'></script>", ""},
+ // Dangling markup in attribute name should strip nonce.
+ {"http://example.test",
+ "<script src='/script' nonce='abc' x<script='foo'></script>", ""},
+ // Duplicate attributes should strip nonce.
+ {"http://example.test",
+ "<script src='/script' nonce='abc' foo='a' foo='b'></script>", ""},
+ // Normal case: nonce preserved when no dangling markup signals.
+ {"http://example.test", "<script src='/script' nonce='abc'></script>",
+ "abc"},
+ // Link with dangling markup should also be stripped.
+ {"http://example.test",
+ "<link rel='stylesheet' href='/style' nonce='abc' bar='<link'>", ""},
+ };
+
+ for (const auto& test_case : test_cases) {
+ SCOPED_TRACE(test_case.input_html);
+ Test(test_case);
+ }
+}
+
TEST_F(HTMLPreloadScannerTest, testAttributionSrc) {
static constexpr bool kSecureDocumentUrl = true;
static constexpr bool kInsecureDocumentUrl = false;
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page