Chrome · Parser
CVE-2026-14058
Logic Error in Parser
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
forthird_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 | |
TokenPreloadScannerthird_party/blink/renderer/core/html/parser/html_preload_scanner.h |
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/frame/csp/content_security_policy_test.ccthird_party/blink/renderer/core/html/parser/html_preload_scanner.ccthird_party/blink/renderer/core/html/parser/html_preload_scanner.hthird_party/blink/renderer/core/html/parser/html_preload_scanner_test.cc
Patch
From fbbde71d8f0cf193c6cb18add3a9fa9dd5f6d54b Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner <[email protected]> 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 <[email protected]> Commit-Queue: Andrew Paseltiner <[email protected]> Reviewed-by: Yoav Weiss (@Shopify) <[email protected]> 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.
References
On This Page