Medium watch CVSS 3.1 chrome Cross Origin 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in CORS
DescriptionMissing authorization in CORS
ComponentCORS
Sub-componentOOR-CORS
Bug ClassCross Origin
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker493322521
Fix commit487365ea7652 (chromium/src) +232/-14
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-17 (per vendor advisory)
Fix landed2026-08-18 (public git)
Disclosed2026-09-08
Patch gap21 days — fix public in source before the advisory

Background

`CorsURLLoader`
the network-service object that enforces CORS/same-origin policy for a resource request before it reaches the network, running in the (browser-side) network service on behalf of a renderer.
`Origin` request header
the header a browser attaches to identify the initiator of a request; servers rely on it for access-control decisions, so it must reflect the true initiator and not be renderer-controlled.
`request_initiator` / `isolated_world_origin` / `IsolationInfo::frame_origin`
the legitimate origins that describe a request’s context: the initiating document, an extension content-script world, and the frame’s isolation origin, respectively.
`ShouldAllowUnsafeHeaders` / `kBypassRequestForbiddenHeadersCheck`
the allowlist mechanism (backed by origin_access_list_) that lets privileged initiators such as permitted extensions set otherwise-forbidden headers.

Root Cause Analysis

CorsURLLoader::StartRequest and the redirect path in FollowRedirect accepted whatever Origin header the renderer supplied without checking that its value matched a legitimate origin for the request. On redirect, the pre-patch code only rejected an Origin modification when kBlockOriginHeaderModificationOnRedirect was enabled and the header was present at all — it never validated the value — and StartRequest had no value check whatsoever, so a compromised renderer could forge an arbitrary Origin (e.g. https://forged.example.com) on a kNoCors request. This violated the invariant that an unprivileged renderer may only assert an Origin of null, its request_initiator, its isolated_world_origin, or IsolationInfo::frame_origin.

The fix adds HasValidOriginHeader, which returns true only for those candidate origins (or when AllowUnsafeHeaders grants a bypass), and calls it in both StartRequest (guarded by kBlockInvalidOriginHeader) and the redirect path (guarded by kBlockInvalidOriginHeaderModificationOnRedirect); an invalid header now fails the request with net::ERR_INVALID_ARGUMENT and reports a bad message. CorsURLLoaderFactory and the new AllowUnsafeHeaders helper were also updated to consult isolated_world_origin (falling back to request_initiator) so extension content scripts are evaluated against the correct origin.

Key insight
The core mistake was trusting a renderer-supplied Origin header value without authorizing it against the request’s real origins; the fix introduces HasValidOriginHeader to reject any Origin that is not null, the initiator, the isolated-world origin, the frame origin, or explicitly allowlisted.

Attack Path

  1. Compromise a renderer An attacker who has compromised a renderer process crafts a ResourceRequest (e.g. mode = kNoCors, credentials_mode = kInclude) whose headers include a forged Origin such as https://forged.example.com.
  2. Send to network service The request is dispatched to CorsURLLoader, whose pre-patch StartRequest performed no validation of the Origin header value and let the forged origin persist.
  3. Bypass server-side origin checks The forged Origin reaches the network with a value the initiator was never entitled to assert, allowing the attacker to impersonate another web origin to cross-origin servers that make access-control decisions from the Origin header (CWE “Missing authorization in CORS”).
  4. Redirect variant Alternatively, via FollowRedirect the renderer supplies a modified Origin header value on redirect that, pre-patch, was not value-checked, achieving the same spoof on the redirected request.

Impact Assessment

A compromised or malicious renderer could forge the Origin header on outgoing requests, spoofing a different web origin to remote servers and defeating origin-based server authorization — a cross-origin authorization bypass carried out from a renderer but taking effect in the network service. The preconditions are the ability to set arbitrary request headers (renderer compromise, or the specific request modes the tests exercise); browser-process requests and allowlisted extension origins are legitimately exempt. The advisory reports this as “Missing authorization in CORS” and does not state exploitation in the wild.

Changed Functions

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

Files Changed

  • services/network/cors/cors_url_loader.h
  • services/network/public/cpp/features.h
  • services/network/cors/cors_url_loader.cc
  • services/network/cors/cors_url_loader_factory.cc
  • services/network/cors/cors_url_loader_unittest.cc
  • services/network/public/cpp/features.cc

Audit Directions

  • Renderer-supplied header values trusted by the network service
    Audit every place the network service forwards security-relevant headers (Origin, Referer, Sec-*) taken from a ResourceRequest without validating the value against the request’s authenticated origins.
  • Presence checks that should be value checks
    Look for guards that only test HasHeader(...) or feature-flag presence and reject/allow wholesale, rather than comparing the header value against an allowlist of legitimate values.
  • Origin selection for extension/isolated worlds
    Review other CORS/forbidden-header paths that use request_initiator alone to ensure they also account for isolated_world_origin, so extension content-script requests are authorized against the correct origin.
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/public/cpp/features.cc b/services/network/public/cpp/features.cc
index c7231a0..ee1dfb97 100644
--- a/services/network/public/cpp/features.cc
+++ b/services/network/public/cpp/features.cc
@@ -494,11 +494,15 @@
 BASE_FEATURE(kBypassRequestForbiddenHeadersCheck,
              base::FEATURE_ENABLED_BY_DEFAULT);
 
-// When enabled, the network service will prohibit modifications to the Origin
-// header in FollowRedirect.
-BASE_FEATURE(kBlockOriginHeaderModificationOnRedirect,
+// When enabled, the network service will prohibit invalid modifications to the
+// Origin header in CorsURLLoader::FollowRedirect.
+BASE_FEATURE(kBlockInvalidOriginHeaderModificationOnRedirect,
              base::FEATURE_ENABLED_BY_DEFAULT);
 
+// When enabled, the network service will prohibit invalid Origin headers in
+// CorsURLLoader::StartRequest.
+BASE_FEATURE(kBlockInvalidOriginHeader, base::FEATURE_ENABLED_BY_DEFAULT);
+
 BASE_FEATURE(kServiceWorkerSyntheticResponseHeaderCheck,
              base::FEATURE_DISABLED_BY_DEFAULT);
 
diff --git a/services/network/public/cpp/features.h b/services/network/public/cpp/features.h
index 57643afb..3081c71 100644
--- a/services/network/public/cpp/features.h
+++ b/services/network/public/cpp/features.h
@@ -321,10 +321,15 @@
 COMPONENT_EXPORT(NETWORK_CPP_FLAGS_AND_SWITCHES)
 BASE_DECLARE_FEATURE(kBypassRequestForbiddenHeadersCheck);
 
-// When enabled, the network service will prohibit modifications to the Origin
-// header in FollowRedirect.
+// When enabled, the network service will prohibit invalid modifications to the
+// Origin header in CorsURLLoader::FollowRedirect.
 COMPONENT_EXPORT(NETWORK_CPP_FLAGS_AND_SWITCHES)
-BASE_DECLARE_FEATURE(kBlockOriginHeaderModificationOnRedirect);
+BASE_DECLARE_FEATURE(kBlockInvalidOriginHeaderModificationOnRedirect);
+
+// When enabled, the network service will prohibit invalid Origin headers in
+// CorsURLLoader::StartRequest.
+COMPONENT_EXPORT(NETWORK_CPP_FLAGS_AND_SWITCHES)
+BASE_DECLARE_FEATURE(kBlockInvalidOriginHeader);
 
 // If enabled, the variations headers for allowlisted domains will be included
 // in the Reporting API uploads.
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…

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

reported by es...@chromium.org

CorsURLLoader might allow an invalid Origin header on a GET/HEAD request

https://source.chromium.org/chromium/chromium/src/+/main:services/network/cors/cors_url_loader.cc;drc=635cc5052af5b0c2d05f9bad46183ab97d2049a5;l=916

This code appears to overwrite an invalid Origin header on e.g. a POST request, but not validate or remove an Origin that a misbehaving renderer might set on e.g. a same-origin GET request. This is a subresource version of the bug that I’m working on fixing in https://chromium-review.git.corp.google.com/c/chromium/src/+/7617635. We should investigate whether this is indeed the case and if so fix it in a similar manner to the navigation request bug.

View on issue tracker