Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactPolicy bypass in Parser
DescriptionPolicy bypass in Parser
ComponentParser
Bug ClassLogic Error
Tracker502354038
Fix commitfbbde71d8f0c (chromium/src) +101/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
for
third_party/blink/renderer/core/frame/csp/content_security_policy.cc
modified
TEST_F
third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc
modified
TokenPreloadScanner
third_party/blink/renderer/core/html/parser/html_preload_scanner.h
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/frame/csp/content_security_policy_test.cc
  • third_party/blink/renderer/core/html/parser/html_preload_scanner.cc
  • third_party/blink/renderer/core/html/parser/html_preload_scanner.h
  • third_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc
From fbbde71d8f0cf193c6cb18add3a9fa9dd5f6d54b Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Mon, 01 Jun 2026 06:41:44 -0700
Subject: [PATCH] Enforce CSP base-uri in HTML preload scanner

The Blink HTML preload scanner failed to validate <base href> tags
against the Content Security Policy 'base-uri' directive. This allowed
an attacker with HTML injection capabilities to redirect speculative
subresource fetches to an external origin, potentially leaking sensitive
relative resource paths and the page URL.

This patch updates TokenPreloadScanner to enforce the 'base-uri' CSP
directive when processing <base> tags.

Two approaches were considered for this fix:

1. Scanner-side validation (Chosen): Validate the <base> tag immediately
   when the scanner encounters it. This ensures the scanner's internal
   base URL state is always CSP-compliant. It prevents all consumers
   of the scanner's base URL (including CSSPreloadScanner) from using
   a malicious origin. This approach requires copying CSP policies to
   the background scanner thread via CachedDocumentParameters.
2. Fetch-side validation: Defer validation until PreloadRequest::Start()
   on the main thread. While this avoids copying CSP data, it would
   result in redundant CSP checks for every relative subresource
   discovered, and it would leave the scanner in a potentially
   vulnerable internal state that could be exploited by future scanner
   features.

Scanner-side validation was chosen for its architectural consistency
with how the main parser handles <base> tags and for its superior
performance characteristics by avoiding redundant checks during resource
resolution.

Fixed: 502354038
Change-Id: Ie157e2351740e65b9f9e1ba97142e39c167764a4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7782696
Reviewed-by: Antonio Sartori <antoniosartori@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Reviewed-by: Yoav Weiss (@Shopify) <yoavweiss@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1639348}
---

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 33399df..06d6e4b 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
@@ -1041,6 +1041,22 @@
                          RedirectStatus::kNoRedirect);
 }
 
+// static
+bool ContentSecurityPolicy::AllowBaseURI(
+    const KURL& url,
+    const Vector<network::mojom::blink::ContentSecurityPolicyPtr>& policies) {
+  for (const auto& policy : policies) {
+    if (!CSPDirectiveListAllowFromSource(
+             *policy, /*policy=*/nullptr, CSPDirectiveName::BaseURI,
+             /*document_url=*/KURL(), url, url, RedirectStatus::kNoRedirect,
+             ReportingDisposition::kSuppressReporting)
+             .IsAllowed()) {
+      return false;
+    }
+  }
+  return true;
+}
+
 bool ContentSecurityPolicy::AllowConnectToSource(
     const KURL& url,
     const KURL& url_before_redirects,
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 df7b086..a7c39c8 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
@@ -413,6 +413,10 @@
   // main world CSP. See ExecutionContext::GetContentSecurityPolicyForWorld.
   static bool ShouldBypassMainWorldDeprecated(const DOMWrapperWorld* world);
 
+  static bool AllowBaseURI(
+      const KURL&,
+      const Vector<network::mojom::blink::ContentSecurityPolicyPtr>&);
+
   static bool IsNonceableElement(const Element*);
 
   static const char* GetDirectiveName(CSPDirectiveName type);
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 789a9fd..40b04c0 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
@@ -1886,4 +1886,24 @@
   }
 }
 
+TEST_F(ContentSecurityPolicyTest, StaticAllowBaseURI) {
+  KURL allowed_base("https://example.test/");
+  KURL blocked_base("https://not-example.test/");
+
+  // Empty policies should allow everything.
+  Vector<network::mojom::blink::ContentSecurityPolicyPtr> empty_policies;
+  EXPECT_TRUE(
+      ContentSecurityPolicy::AllowBaseURI(allowed_base, empty_policies));
+  EXPECT_TRUE(
+      ContentSecurityPolicy::AllowBaseURI(blocked_base, empty_policies));
+
+  // Policy with base-uri 'self'.
+  Vector<network::mojom::blink::ContentSecurityPolicyPtr> policies =
+      ParseContentSecurityPolicies(
+          "base-uri 'self'", ContentSecurityPolicyType::kEnforce,
+          ContentSecurityPolicySource::kHTTP, *secure_origin);
+  EXPECT_TRUE(ContentSecurityPolicy::AllowBaseURI(allowed_base, policies));
+  EXPECT_FALSE(ContentSecurityPolicy::AllowBaseURI(blocked_base, policies));
+}
+
 }  // namespace blink
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 58e071db..88c7e7d 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
@@ -33,6 +33,7 @@
 
 #include "base/task/sequenced_task_runner.h"
 #include "base/trace_event/trace_event.h"
+#include "services/network/public/mojom/content_security_policy.mojom-blink.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h"
 #include "third_party/blink/public/mojom/script/script_type.mojom-blink.h"
@@ -45,6 +46,7 @@
 #include "third_party/blink/renderer/core/css/parser/sizes_attribute_parser.h"
 #include "third_party/blink/renderer/core/dom/document.h"
 #include "third_party/blink/renderer/core/execution_context/security_context.h"
+#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/core/frame/settings.h"
 #include "third_party/blink/renderer/core/frame/viewport_data.h"
@@ -1161,7 +1163,10 @@
     KURL url(document_url_,
              StripLeadingAndTrailingHtmlSpaces(href_attribute->Value()));
     bool is_valid_base_url =
-        url.IsValid() && !url.ProtocolIsData() && !url.ProtocolIsJavaScript();
+        url.IsValid() && !url.ProtocolIsData() && !url.ProtocolIsJavaScript() &&
+        ContentSecurityPolicy::AllowBaseURI(
+            url, document_parameters_->content_security_policy);
+
     predicted_base_element_url_ = is_valid_base_url ? url : KURL();
   }
 }
@@ -1342,6 +1347,14 @@
           : kPreloadLazyLoadImageType;
   probe::GetDisabledImageTypes(document->GetExecutionContext(),
                                &disabled_image_types);
+  if (document->GetExecutionContext() &&
+      document->GetExecutionContext()->GetContentSecurityPolicy()) {
+    for (const auto& policy : document->GetExecutionContext()
+                                  ->GetContentSecurityPolicy()
+                                  ->GetParsedPolicies()) {
+      content_security_policy.push_back(policy->Clone());
+    }
+  }
 }
 
 // static
diff --git a/third_party/blink/renderer/core/html/parser/html_preload_scanner.h b/third_party/blink/renderer/core/html/parser/html_preload_scanner.h
index 9f2d424..c04eb57a 100644
--- a/third_party/blink/renderer/core/html/parser/html_preload_scanner.h
+++ b/third_party/blink/renderer/core/html/parser/html_preload_scanner.h
@@ -35,6 +35,7 @@
 #include "base/memory/weak_ptr.h"
 #include "base/task/sequenced_task_runner.h"
 #include "services/network/public/cpp/client_hints.h"
+#include "services/network/public/mojom/content_security_policy.mojom-blink.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/css/media_values_cached.h"
@@ -103,6 +104,8 @@
   static std::optional<features::LcppPreloadLazyLoadImageType>
       preload_lazy_load_image_type_for_testing;
   HashSet<String> disabled_image_types;
+  Vector<network::mojom::blink::ContentSecurityPolicyPtr>
+      content_security_policy;
 };
 
 class TokenPreloadScanner {
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 821b996..5dc7b2b 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
@@ -14,6 +14,7 @@
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/platform/web_runtime_features.h"
 #include "third_party/blink/renderer/core/css/media_values_cached.h"
+#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/settings.h"
 #include "third_party/blink/renderer/core/html/cross_origin_attribute.h"
@@ -28,6 +29,7 @@
 #include "third_party/blink/renderer/platform/exported/wrapped_resource_response.h"
 #include "third_party/blink/renderer/platform/loader/fetch/client_hints_preferences.h"
 #include "third_party/blink/renderer/platform/network/http_names.h"
+#include "third_party/blink/renderer/platform/network/http_parsers.h"
 #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
Loading diff…

Regression Test / PoC

shipped with the fix
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 789a9fd..40b04c0 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
@@ -1886,4 +1886,24 @@
   }
 }
 
+TEST_F(ContentSecurityPolicyTest, StaticAllowBaseURI) {
+  KURL allowed_base("https://example.test/");
+  KURL blocked_base("https://not-example.test/");
+
+  // Empty policies should allow everything.
+  Vector<network::mojom::blink::ContentSecurityPolicyPtr> empty_policies;
+  EXPECT_TRUE(
+      ContentSecurityPolicy::AllowBaseURI(allowed_base, empty_policies));
+  EXPECT_TRUE(
+      ContentSecurityPolicy::AllowBaseURI(blocked_base, empty_policies));
+
+  // Policy with base-uri 'self'.
+  Vector<network::mojom::blink::ContentSecurityPolicyPtr> policies =
+      ParseContentSecurityPolicies(
+          "base-uri 'self'", ContentSecurityPolicyType::kEnforce,
+          ContentSecurityPolicySource::kHTTP, *secure_origin);
+  EXPECT_TRUE(ContentSecurityPolicy::AllowBaseURI(allowed_base, policies));
+  EXPECT_FALSE(ContentSecurityPolicy::AllowBaseURI(blocked_base, policies));
+}
+
 }  // namespace blink
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 821b996..5dc7b2b 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
@@ -14,6 +14,7 @@
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/platform/web_runtime_features.h"
 #include "third_party/blink/renderer/core/css/media_values_cached.h"
+#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/settings.h"
 #include "third_party/blink/renderer/core/html/cross_origin_attribute.h"
@@ -28,6 +29,7 @@
 #include "third_party/blink/renderer/platform/exported/wrapped_resource_response.h"
 #include "third_party/blink/renderer/platform/loader/fetch/client_hints_preferences.h"
 #include "third_party/blink/renderer/platform/network/http_names.h"
+#include "third_party/blink/renderer/platform/network/http_parsers.h"
 #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
 #include "third_party/blink/renderer/platform/testing/url_loader_mock_factory.h"
 #include "third_party/blink/renderer/platform/testing/url_test_helpers.h"
@@ -575,7 +577,7 @@
         &GetDocument(), test_case.expected_browsing_topics);
   }
 
- private:
+ protected:
   std::unique_ptr<HTMLPreloadScanner> scanner_;
 };
 
@@ -1238,6 +1240,47 @@
   }
 }
 
+TEST_F(HTMLPreloadScannerTest, BaseURICSPEnforcement) {
+  // Regression test for crbug.com/502354038.
+  KURL document_url("http://whatever.test/");
+  NavigateTo(document_url);
+
+  // Set up the document with a CSP that restricts base-uri to 'none'.
+  GetDocument().GetExecutionContext()->GetContentSecurityPolicy()->AddPolicies(
+      ParseContentSecurityPolicies(
+          "base-uri 'none'",
+          network::mojom::blink::ContentSecurityPolicyType::kEnforce,
+          network::mojom::blink::ContentSecurityPolicySource::kHTTP,
+          document_url));
+
+  // Create the scanner manually after setting up the CSP.
+  HTMLParserOptions options(&GetDocument());
+  scanner_ = std::make_unique<HTMLPreloadScanner>(
+      std::make_unique<HTMLTokenizer>(options), document_url,
+      std::make_unique<CachedDocumentParameters>(&GetDocument()),
+      CreateMediaValuesData(), TokenPreloadScanner::ScannerType::kMainDocument,
+      /* script_token_scanner=*/nullptr,
+      /* take_preload=*/HTMLPreloadScanner::TakePreloadFn(),
+      Vector<ElementLocator>());
+
+  HTMLMockHTMLResourcePreloader preloader(GetDocument().Url());
+
+  // HTML with a cross-origin <base> tag and a relative image.
+  // The <base> tag should be blocked by CSP, so the image should be resolved
+  // against the document URL, not the blocked base URL.
+  scanner_->AppendToEnd(
+      String("<base href='http://attacker.test/'><img src='test.png'>"));
+
+  std::unique_ptr<PendingPreloadData> preload_data = scanner_->Scan(KURL());
+  preloader.TakePreloadData(std::move(preload_data));
+
+  // EXPECTATION: The base URL used for the preload request should be the
+  // original document base URL, NOT the attacker-controlled one from the <base>
+  // tag, because 'http://attacker.test/' violates 'base-uri 'none''.
+  preloader.PreloadRequestVerification(ResourceType::kImage, "test.png",
+                                       nullptr, 0, ClientHintsPreferences());
+}
+
 TEST_F(HTMLPreloadScannerTest, testNonce) {
   NonceTestCase test_cases[] = {
       {"http://example.test", "<script src='/script'></script>", ""},
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.