Chrome · CORS
CVE-2026-87434
Cross Origin in CORS
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
forservices/network/cors/cors_url_loader.cc |
modified | |
ifservices/network/cors/cors_url_loader.cc |
modified |
Files Changed
services/network/cors/cors_url_loader.ccservices/network/cors/cors_url_loader.hservices/network/cors/cors_url_loader_factory.ccservices/network/cors/cors_url_loader_unittest.cc
Patch
From 487365ea7652dc23a26d7a0bf8473068849ae597 Mon Sep 17 00:00:00 2001 From: Takashi Toyoshima <[email protected]> Date: Mon, 17 Aug 2026 22:19:55 -0700 Subject: [PATCH] OOR-CORS: Block invalid Origin headers in CorsURLLoader Prevents compromised renderers from spoofing Origin headers in CorsURLLoader::StartRequest and FollowRedirect. Unprivileged renderers may only specify an Origin matching: - "null" (for tainted / opaque origins) - request_initiator - isolated_world_origin - IsolationInfo::frame_origin Any unexpected Origin header triggers mojo::ReportBadMessage and fails the request with net::ERR_INVALID_ARGUMENT. Bug: 493322521 Change-Id: I5538010f81723f29e65e8ce334bda1f932d5b217 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8214760 Reviewed-by: Kenichi Ishibashi <[email protected]> Commit-Queue: Takashi Toyoshima <[email protected]> Cr-Commit-Position: refs/heads/main@{#1681113} --- diff --git a/services/network/cors/cors_url_loader.cc b/services/network/cors/cors_url_loader.cc index 7d31e958..c56255c50 100644 --- a/services/network/cors/cors_url_loader.cc +++ b/services/network/cors/cors_url_loader.cc @@ -489,13 +489,16 @@ } } - if (base::FeatureList::IsEnabled( - features::kBlockOriginHeaderModificationOnRedirect) && - headers_update_params.modified_headers.HasHeader( - net::HttpRequestHeaders::kOrigin)) { + std::optional<std::string> modified_origin_header = + headers_update_params.modified_headers.GetHeader( + net::HttpRequestHeaders::kOrigin); + if (modified_origin_header && + base::FeatureList::IsEnabled( + features::kBlockInvalidOriginHeaderModificationOnRedirect) && + !HasValidOriginHeader(*modified_origin_header)) { HandleComplete(URLLoaderCompletionStatus(net::ERR_INVALID_ARGUMENT)); mojo::ReportBadMessage( - "CorsURLLoader: Origin header modification on redirect is not " + "CorsURLLoader: Invalid Origin header modification on redirect is not " "permitted"); return; } @@ -936,6 +939,41 @@ /*cookie_partition_key=*/std::nullopt, request_.permissions_policy); } +bool CorsURLLoader::AllowUnsafeHeaders() const { + return process_id_.is_browser() || + cors::ShouldAllowUnsafeHeaders(*origin_access_list_, + request_.isolated_world_origin + ? request_.isolated_world_origin + : request_.request_initiator, + request_.url); +} + +bool CorsURLLoader::HasValidOriginHeader( + const std::string& origin_header_value) const { + if (AllowUnsafeHeaders()) { + return true; + } + + // "null" is always allowed (e.g. tainted or opaque origins). + if (origin_header_value == url::Origin().Serialize()) { + return true; + } + + // Check against legitimate candidate origins for this request context. + const std::optional<url::Origin> candidate_origins[] = { + request_.isolated_world_origin, + request_.request_initiator, + isolation_info_.frame_origin(), + }; + for (const auto& origin : candidate_origins) { + if (origin.has_value() && origin_header_value == origin->Serialize()) { + return true; + } + } + + return false; +} + void CorsURLLoader::StartRequest() { TRACE_EVENT("loading", "CorsURLLoader::StartRequest", net::NetLogWithSourceToFlow(net_log_)); @@ -978,6 +1016,18 @@ request_.method != net::HttpRequestHeaders::kHeadMethod; }; + std::optional<std::string> origin_header_value = + request_.headers.GetHeader(net::HttpRequestHeaders::kOrigin); + if (origin_header_value && + base::FeatureList::IsEnabled(features::kBlockInvalidOriginHeader) && + !HasValidOriginHeader(*origin_header_value)) { + HandleComplete(URLLoaderCompletionStatus(net::ERR_INVALID_ARGUMENT)); + mojo::ReportBadMessage( + "CorsURLLoader: Invalid Origin header is not permitted for this " + "request"); + return; + } + if (should_include_origin_header()) { // If the Origin header is given, check if the initiator has a permission to // override unsafe headers for the target URL. This Allowlist is given from @@ -985,8 +1035,7 @@ // security check here in the network service. const bool has_custom_origin_header_with_bypass = request_.headers.HasHeader(net::HttpRequestHeaders::kOrigin) && - cors::ShouldAllowUnsafeHeaders( - *origin_access_list_, request_.request_initiator, request_.url); + AllowUnsafeHeaders(); if (!has_custom_origin_header_with_bypass) { if (tainted_) { diff --git a/services/network/cors/cors_url_loader.h b/services/network/cors/cors_url_loader.h index 0d58b86..4ebb17cf 100644 --- a/services/network/cors/cors_url_loader.h +++ b/services/network/cors/cors_url_loader.h @@ -142,6 +142,12 @@ // and `isolation_info_`. std::optional<net::cookie_util::StorageAccessStatus> GetStorageAccessStatus(); + // Checks if the current request is allowed to override unsafe headers. + bool AllowUnsafeHeaders() const; + + // Validates whether `origin_header_value` is permitted for `request_`. + bool HasValidOriginHeader(const std::string& origin_header_value) const; + void StartRequest(); // Helper for `OnPreflightRequestComplete()`. diff --git a/services/network/cors/cors_url_loader_factory.cc b/services/network/cors/cors_url_loader_factory.cc index f71710c..78442c82d 100644 --- a/services/network/cors/cors_url_loader_factory.cc +++ b/services/network/cors/cors_url_loader_factory.cc @@ -896,7 +896,10 @@ } const bool allow_unsafe_headers = cors::ShouldAllowUnsafeHeaders( - *origin_access_list_, request.request_initiator, request.url); + *origin_access_list_, + request.isolated_world_origin ? request.isolated_world_origin + : request.request_initiator, + request.url); std::string forbidden_header; if (!process_id_.is_browser() && !allow_unsafe_headers && ContainsForbiddenSecurityHeader(request.headers, &forbidden_header)) { diff --git a/services/network/cors/cors_url_loader_unittest.cc b/services/network/cors/cors_url_loader_unittest.cc index 4a571f8..9bea7e68 100644 --- a/services/network/cors/cors_url_loader_unittest.cc +++ b/services/network/cors/cors_url_loader_unittest.cc @@ -40,6 +40,7 @@ #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "url/origin.h" +#include "url/url_util.h" namespace network::cors { namespace { @@ -476,6 +477,156 @@ GetRequest().headers.HasHeader(net::HttpRequestHeaders::kOrigin)); } +TEST_F(CorsURLLoaderTest, + CrossOriginRequestWithNoCorsModeAndForgedOriginHeader) { + const GURL origin("https://example.com"); + const GURL url("http://other.example.com/foo.png"); + ResourceRequest request; + request.mode = mojom::RequestMode::kNoCors; + request.credentials_mode = mojom::CredentialsMode::kInclude; + request.method = "GET"; + request.url = url; + request.request_initiator = url::Origin::Create(origin); + request.headers.SetHeader(net::HttpRequestHeaders::kOrigin, + "https://forged.example.com"); + + BadMessageTestHelper bad_message_helper; + CreateLoaderAndStart(request); + + RunUntilComplete(); + + EXPECT_FALSE(IsNetworkLoaderStarted()); + EXPECT_FALSE(client().has_received_redirect()); + EXPECT_FALSE(client().has_received_response()); + EXPECT_TRUE(client().has_received_completion()); + EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client().completion_status().error_code); + EXPECT_THAT( + bad_message_helper.bad_message_reports(), + ElementsAre( + "CorsURLLoader: Invalid Origin header is not permitted for this " + "request")); +}
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/services/network/cors/cors_url_loader_unittest.cc b/services/network/cors/cors_url_loader_unittest.cc
index 4a571f8..9bea7e68 100644
--- a/services/network/cors/cors_url_loader_unittest.cc
+++ b/services/network/cors/cors_url_loader_unittest.cc
@@ -40,6 +40,7 @@
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/origin.h"
+#include "url/url_util.h"
namespace network::cors {
namespace {
@@ -476,6 +477,156 @@
GetRequest().headers.HasHeader(net::HttpRequestHeaders::kOrigin));
}
+TEST_F(CorsURLLoaderTest,
+ CrossOriginRequestWithNoCorsModeAndForgedOriginHeader) {
+ const GURL origin("https://example.com");
+ const GURL url("http://other.example.com/foo.png");
+ ResourceRequest request;
+ request.mode = mojom::RequestMode::kNoCors;
+ request.credentials_mode = mojom::CredentialsMode::kInclude;
+ request.method = "GET";
+ request.url = url;
+ request.request_initiator = url::Origin::Create(origin);
+ request.headers.SetHeader(net::HttpRequestHeaders::kOrigin,
+ "https://forged.example.com");
+
+ BadMessageTestHelper bad_message_helper;
+ CreateLoaderAndStart(request);
+
+ RunUntilComplete();
+
+ EXPECT_FALSE(IsNetworkLoaderStarted());
+ EXPECT_FALSE(client().has_received_redirect());
+ EXPECT_FALSE(client().has_received_response());
+ EXPECT_TRUE(client().has_received_completion());
+ EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client().completion_status().error_code);
+ EXPECT_THAT(
+ bad_message_helper.bad_message_reports(),
+ ElementsAre(
+ "CorsURLLoader: Invalid Origin header is not permitted for this "
+ "request"));
+}
+
+TEST_F(
+ CorsURLLoaderTest,
+ CrossOriginRequestWithNoCorsModeAndForgedOriginHeaderFromBrowserProcess) {
+ const GURL origin("https://example.com");
+ const GURL url("http://other.example.com/foo.png");
+ ResetFactory(url::Origin::Create(origin), OriginatingProcessId::browser());
+
+ ResourceRequest request;
+ request.mode = mojom::RequestMode::kNoCors;
+ request.credentials_mode = mojom::CredentialsMode::kInclude;
+ request.method = "GET";
+ request.url = url;
+ request.request_initiator = url::Origin::Create(origin);
+ request.headers.SetHeader(net::HttpRequestHeaders::kOrigin,
+ "https://forged.example.com");
+ CreateLoaderAndStart(request);
+ RunUntilCreateLoaderAndStartCalled();
+
+ NotifyLoaderClientOnReceiveResponse();
+ NotifyLoaderClientOnComplete(net::OK);
+
+ RunUntilComplete();
+
+ EXPECT_TRUE(IsNetworkLoaderStarted());
+ EXPECT_FALSE(client().has_received_redirect());
+ EXPECT_TRUE(client().has_received_response());
+ EXPECT_TRUE(client().has_received_completion());
+ EXPECT_EQ(net::OK, client().completion_status().error_code);
+ EXPECT_EQ(GetRequest().headers.GetHeader(net::HttpRequestHeaders::kOrigin),
+ "https://forged.example.com");
+}
+
+TEST_F(
+ CorsURLLoaderTest,
+ CrossOriginRequestWithNoCorsModeAndForgedOriginHeaderFromExtensionWithPermission) {
+ url::ScopedSchemeRegistryForTests scoped_registry;
+ url::AddStandardScheme("chrome-extension", url::SCHEME_WITH_HOST);
+
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ features::kBypassRequestForbiddenHeadersCheck);
+
+ const GURL origin("https://example.com");
+ const GURL url("http://other.example.com/foo.png");
+ const url::Origin extension_origin =
+ url::Origin::Create(GURL("chrome-extension://abcdefghijklmnop"));
+
+ ResetFactoryParams factory_params;
+ factory_params.ignore_isolated_world_origin = false;
+ ResetFactory(url::Origin::Create(origin), kRendererProcessId, factory_params);
+
+ AddAllowListEntryForOrigin(extension_origin, std::string(url.scheme()),
+ std::string(url.host()),
+ mojom::CorsDomainMatchMode::kDisallowSubdomains);
+
+ ResourceRequest request;
+ request.mode = mojom::RequestMode::kNoCors;
+ request.credentials_mode = mojom::CredentialsMode::kInclude;
+ request.method = "GET";
+ request.url = url;
+ request.request_initiator = url::Origin::Create(origin);
+ request.isolated_world_origin = extension_origin;
+ request.headers.SetHeader(net::HttpRequestHeaders::kOrigin,
+ "chrome-extension://abcdefghijklmnop");
+ CreateLoaderAndStart(request);
+ RunUntilCreateLoaderAndStartCalled();
+
+ NotifyLoaderClientOnReceiveResponse();
+ NotifyLoaderClientOnComplete(net::OK);
+
+ RunUntilComplete();
+
+ EXPECT_TRUE(IsNetworkLoaderStarted());
+ EXPECT_FALSE(client().has_received_redirect());
+ EXPECT_TRUE(client().has_received_response());
+ EXPECT_TRUE(client().has_received_completion());
+ EXPECT_EQ(net::OK, client().completion_status().error_code);
+ EXPECT_EQ(GetRequest().headers.GetHeader(net::HttpRequestHeaders::kOrigin),
+ "chrome-extension://abcdefghijklmnop");
+}
+
+TEST_F(CorsURLLoaderTest,
+ CrossOriginRequestWithNoCorsModeAndForgedOriginHeaderOnRedirect) {
+ const GURL origin("https://example.com");
+ const GURL url("https://example.com/foo.png");
+ ResourceRequest request;
+ request.mode = mojom::RequestMode::kNoCors;
+ request.credentials_mode = mojom::CredentialsMode::kInclude;
+ request.method = "GET";
+ request.url = url;
+ request.request_initiator = url::Origin::Create(origin);
+ CreateLoaderAndStart(request);
+ RunUntilCreateLoaderAndStartCalled();
+ EXPECT_EQ(1, num_created_loaders());
+
+ NotifyLoaderClientOnReceiveRedirect(
+ CreateRedirectInfo(301, "GET", GURL("https://example.com/bar.png")));
+ RunUntilRedirectReceived();
+
+ ASSERT_TRUE(IsNetworkLoaderStarted());
+ EXPECT_TRUE(client().has_received_redirect());
+ EXPECT_FALSE(client().has_received_response());
+ EXPECT_FALSE(client().has_received_completion());
+
+ BadMessageTestHelper bad_message_helper;
+ network::HttpRequestHeadersUpdateParams headers_update_params;
+ headers_update_params.modified_headers.SetHeader(
+ net::HttpRequestHeaders::kOrigin, "https://forged.example.com");
+ FollowRedirect(std::move(headers_update_params));
+ RunUntilComplete();
+
+ EXPECT_FALSE(client().has_received_response());
+ EXPECT_TRUE(client().has_received_completion());
+ EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client().completion_status().error_code);
+ EXPECT_THAT(
+ bad_message_helper.bad_message_reports(),
+ ElementsAre("CorsURLLoader: Invalid Origin header modification on "
+ "redirect is not permitted"));
+}
+
TEST_F(CorsURLLoaderTest, CrossOriginRequestWithNoCorsModeAndPatchMethod) {
const GURL origin("https://example.com");
const GURL url("http://other.example.com/foo.png");
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