Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Parser
DescriptionInappropriate implementation in Parser
ComponentParser
Bug ClassLogic Error
Tracker514424283
Fix commit53096e81cb32 (chromium/src) +74/-11
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/frame/csp/content_security_policy.cc
modified
for
third_party/blink/renderer/core/html/parser/html_preload_scanner.cc
modified
TEST_F
third_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc
modified
for
third_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/frame/csp/content_security_policy.cc
  • third_party/blink/renderer/core/frame/csp/content_security_policy.h
  • third_party/blink/renderer/core/html/parser/html_preload_scanner.cc
  • third_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc
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.