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 <leolee@microsoft.com>
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 <daniec@microsoft.com>
Commit-Queue: Leo Lee <leolee@microsoft.com>
Reviewed-by: Jacques Newman <janewman@microsoft.com>
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