Medium chrome Cross Origin 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in CORS
DescriptionMissing authorization in CORS
ComponentCORS
Bug ClassCross Origin
Tracker493322521
Fix commit487365ea7652 (chromium/src) +232/-14
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
for
services/network/cors/cors_url_loader.cc
modified
if
services/network/cors/cors_url_loader.cc
modified

Files Changed

  • services/network/cors/cors_url_loader.cc
  • services/network/cors/cors_url_loader.h
  • services/network/cors/cors_url_loader_factory.cc
  • services/network/cors/cors_url_loader_unittest.cc
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.