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 <toyoshim@chromium.org>
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 <bashi@chromium.org>
Commit-Queue: Takashi Toyoshima <toyoshim@chromium.org>
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