Medium watch CVSS 6.5 chrome Cross Origin 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in CORS
DescriptionMissing authorization in CORS
ComponentCORS
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.
Tracker554038924
Fix commit22d40030760c (chromium/src) +331/-0
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedAnonymous
Reported2026-08-28 (per vendor advisory)
Fix landed2026-09-16 (public git)
Disclosed2026-09-29
Patch gap13 days — fix public in source before the advisory

Background

`CorsURLLoaderFactory`
the network-service object that validates each ResourceRequest from a renderer (via IsValidRequest) before creating a CorsURLLoader, enforcing per-factory origin locks.
`request_initiator_origin_lock_`
the origin a factory is bound to, against which request.request_initiator is already validated; the pre-patch code enforced this but had no equivalent lock for isolated-world origins.
`isolated_world_origin`
a ResourceRequest field naming the extension/content-script (isolated world) origin, used by downstream checks such as ShouldAllowUnsafeHeaders to grant extension privileges (CORS/ORB relaxations, Sec- headers).
`ignore_isolated_world_origin`
a factory flag that is true for normal web-page factories (extension origins are meaningless) and false for privileged isolated-world factories.

Root Cause Analysis

CorsURLLoaderFactory::IsValidRequest validated request.request_initiator against request_initiator_origin_lock_ but never bound or validated request.isolated_world_origin to any lock.

As a result, a compromised renderer holding a privileged factory (ignore_isolated_world_origin_ false) could set isolated_world_origin to an arbitrary extension origin, and normal web-page factories (ignore_isolated_world_origin_ true) never cleared a spoofed value either, so downstream consumers like ShouldAllowUnsafeHeaders and CorsURLLoader would treat the request as originating from a trusted isolated world. This let the renderer bypass CORS/ORB or forge Sec- headers under a spoofed extension identity.

The fix introduces isolated_world_origin_lock in URLLoaderFactoryParams, populated in url_loader_factory_params_helper.cc for isolated-world factories, and enforces it in IsValidRequest: when ignore_isolated_world_origin_ is true the field is normalized to std::nullopt, and when it is false a non-empty isolated_world_origin from a renderer must be same-origin with isolated_world_origin_lock_ or the request is rejected via mojo::ReportBadMessage and IsValidRequest returns false. This restores the invariant that a request’s claimed isolated-world origin must match the origin the factory was provisioned for.

Key insight
The core mistake was enforcing an origin lock on request_initiator while leaving the equally trust-bearing isolated_world_origin completely unvalidated; the fix adds a matching per-factory lock and rejects (or clears) any isolated-world origin the renderer cannot prove it owns.

Attack Path

  1. Obtain a privileged factory A compromised renderer that legitimately holds an isolated-world CorsURLLoaderFactory (ignore_isolated_world_origin_ false) sends a ResourceRequest through it. (inferred that the renderer is compromised, per the advisory’s threat model.)
  2. Spoof the isolated-world origin The renderer sets request.isolated_world_origin to an arbitrary extension origin it does not actually own; pre-patch IsValidRequest performs no check against any lock.
  3. Downstream trust granted Consumers such as ShouldAllowUnsafeHeaders accept the spoofed origin and grant its allow-list privileges, permitting forbidden Sec- headers or relaxed CORS/ORB handling.
  4. Cross-origin access / header forgery The request proceeds with the spoofed extension’s privileges, bypassing CORS/ORB restrictions or forging Sec- headers under that identity.

Impact Assessment

A compromised renderer gains the ability to impersonate arbitrary extension (isolated-world) origins in the network service, bypassing CORS/ORB authorization and forging Sec- headers, which the advisory classifies as missing authorization in CORS. Exploitation requires the renderer to already be able to drive a privileged isolated-world factory; the fix is gated behind the kEnforceIsolatedWorldOriginLock kill-switch feature (enabled by default). No exploited-in-the-wild status is stated in the advisory.

Changed Functions

FunctionChangeNotes
if
content/browser/url_loader_factory_params_helper.cc
modified
if
services/network/cors/cors_url_loader_factory.cc
modified
GURL
services/network/cors/cors_url_loader_test_util.h
modified
IsolatedWorldOriginLockCorsURLLoaderFactoryTest
services/network/cors/cors_url_loader_factory_unittest.cc
modified
TEST_F
services/network/cors/cors_url_loader_factory_unittest.cc
modified

Files Changed

  • services/network/cors/cors_url_loader_test_util.h
  • services/network/public/cpp/features.h
  • services/network/cors/cors_url_loader_factory.h
  • services/network/public/mojom/network_context.mojom
  • services/network/cors/cors_url_loader_factory.cc
  • services/network/cors/cors_url_loader_unittest.cc
  • content/browser/url_loader_factory_params_helper.cc
  • services/network/cors/cors_url_loader_test_util.cc
  • services/network/cors/cors_url_loader_factory_unittest.cc
  • services/network/public/cpp/features.cc

Audit Directions

  • Trust-bearing request fields without a factory lock
    Enumerate every ResourceRequest field consumed for authorization decisions (origins, initiators, header allow-lists) and confirm each is validated against a browser-provisioned per-factory lock, not accepted verbatim from the renderer.
  • Asymmetric validation of paired fields
    Where one field (e.g. request_initiator) is locked but a semantically similar field (e.g. isolated_world_origin) is not, treat the unlocked one as a spoofing vector.
  • Downstream privilege consumers
    Audit ShouldAllowUnsafeHeaders and other checks that key off isolated-world/extension origins to ensure they never see an origin the factory did not authorize, including the “ignore” case where a stale value must be cleared rather than left set.
From 22d40030760cb88140206e0b5573eee00e1b13fb Mon Sep 17 00:00:00 2001
From: Takashi Toyoshima <[email protected]>
Date: Tue, 15 Sep 2026 23:50:58 -0700
Subject: [PATCH] Enforce isolated_world_origin lock on CorsURLLoaderFactory

Previously, CorsURLLoaderFactory validated request_initiator against
request_initiator_origin_lock_, but did not bind or validate
request.isolated_world_origin against a specific origin lock when
ignore_isolated_world_origin was false. This could allow a compromised
renderer using a privileged factory to spoof arbitrary extension origins
via isolated_world_origin to bypass CORS/ORB or forge Sec- headers.

This CL introduces isolated_world_origin_lock in
network.mojom.URLLoaderFactoryParams and enforces origin locking:
1. In CorsURLLoaderFactory::IsValidRequest, if
   ignore_isolated_world_origin_ is true (normal web page factories),
   any provided request.isolated_world_origin is normalized to nullopt,
   ensuring downstream checks (e.g. ShouldAllowUnsafeHeaders) and
   CorsURLLoader cannot be spoofed.
2. If ignore_isolated_world_origin_ is false (privileged isolated world
   factories), any non-empty request.isolated_world_origin must match
   isolated_world_origin_lock_. A mismatch from a renderer process
   triggers mojo::ReportBadMessage.
3. Guarded by the kEnforceIsolatedWorldOriginLock kill-switch feature.

Bug: 554038924
Change-Id: Ie6f3f7f6837428df66cb13f2fc1cb5982e4dd89c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8402138
Reviewed-by: Kenichi Ishibashi <[email protected]>
Reviewed-by: Rakina Zata Amni <[email protected]>
Commit-Queue: Takashi Toyoshima <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1698684}
---
diff --git a/content/browser/url_loader_factory_params_helper.cc b/content/browser/url_loader_factory_params_helper.cc
index a3c9753..7fadfe3 100644
--- a/content/browser/url_loader_factory_params_helper.cc
+++ b/content/browser/url_loader_factory_params_helper.cc
@@ -135,6 +135,10 @@
       process->GetBrowserContext(), origin, is_for_isolated_world,
       is_for_service_worker, params.get());
 
+  if (is_for_isolated_world && !params->ignore_isolated_world_origin) {
+    params->isolated_world_origin_lock = origin;
+  }
+
   params->cookie_observer = std::move(cookie_observer);
   params->trust_token_observer = std::move(trust_token_observer);
   params->shared_dictionary_observer = std::move(shared_dictionary_observer);
diff --git a/services/network/cors/cors_url_loader_factory.cc b/services/network/cors/cors_url_loader_factory.cc
index 7abdda65..21ae5ad 100644
--- a/services/network/cors/cors_url_loader_factory.cc
+++ b/services/network/cors/cors_url_loader_factory.cc
@@ -118,6 +118,18 @@
   return crash_key;
 }
 
+base::debug::CrashKeyString* GetIsolatedWorldOriginLockCrashKey() {
+  static auto* crash_key = base::debug::AllocateCrashKeyString(
+      "isolated_world_origin_lock", base::debug::CrashKeySize::Size256);
+  return crash_key;
+}
+
+base::debug::CrashKeyString* GetIsolatedWorldOriginCrashKey() {
+  static auto* crash_key = base::debug::AllocateCrashKeyString(
+      "isolated_world_origin", base::debug::CrashKeySize::Size256);
+  return crash_key;
+}
+
 bool IsTrustedNavigationRequestFromSecureContext(
     const ResourceRequest& request) {
   if (!request.trusted_params) {
@@ -224,6 +236,9 @@
       process_id_(params->process_id),
       request_initiator_origin_lock_(params->request_initiator_origin_lock),
       ignore_isolated_world_origin_(params->ignore_isolated_world_origin),
+      isolated_world_origin_lock_(params->ignore_isolated_world_origin
+                                      ? std::nullopt
+                                      : params->isolated_world_origin_lock),
       trust_token_issuance_policy_(params->trust_token_issuance_policy),
       trust_token_redemption_policy_(params->trust_token_redemption_policy),
       isolation_info_(params->isolation_info),
@@ -261,6 +276,12 @@
   DCHECK(process_id_);
   DCHECK_EQ(net::IsolationInfo::RequestType::kOther,
             params->isolation_info.request_type());
+  if (params->ignore_isolated_world_origin &&
+      params->isolated_world_origin_lock.has_value()) {
+    mojo::ReportBadMessage(
+        "CorsURLLoaderFactory: isolated_world_origin_lock set when "
+        "ignore_isolated_world_origin is true");
+  }
   if (context_->url_request_context()->bound_network() !=
       net::handles::kInvalidNetworkHandle) {
     // CorsURLLoaderFactories bound to a network allow CORS preflight load
@@ -893,6 +914,32 @@
       return false;
   }
 
+  if (ignore_isolated_world_origin_) {
+    // Normal web frame factories ignore isolated world origins. Clear any
+    // spoofed isolated_world_origin from the renderer so that downstream checks
+    // (e.g. ShouldAllowUnsafeHeaders) and CorsURLLoader do not see it.
+    request.isolated_world_origin = std::nullopt;
+  } else if (!process_id_.is_browser() && request.isolated_world_origin &&
+             base::FeatureList::IsEnabled(
+                 features::kEnforceIsolatedWorldOriginLock)) {
+    if (!isolated_world_origin_lock_ ||
+        !isolated_world_origin_lock_->IsSameOriginWith(
+            *request.isolated_world_origin)) {
+      url::debug::ScopedOriginCrashKey initiator_origin_lock_crash_key(
+          GetRequestInitiatorOriginLockCrashKey(),
+          base::OptionalToPtr(request_initiator_origin_lock_));
+      url::debug::ScopedOriginCrashKey isolated_origin_lock_crash_key(
+          GetIsolatedWorldOriginLockCrashKey(),
+          base::OptionalToPtr(isolated_world_origin_lock_));
+      url::debug::ScopedOriginCrashKey isolated_origin_crash_key(
+          GetIsolatedWorldOriginCrashKey(),
+          base::OptionalToPtr(request.isolated_world_origin));
+      mojo::ReportBadMessage(
+          "CorsURLLoaderFactory: isolated_world_origin lock mismatch");
+      return false;
+    }
+  }
+
   if (!IsValidCorsExemptHeaders(*context_->cors_exempt_header_list(),
                                 request.cors_exempt_headers)) {
     return false;
diff --git a/services/network/cors/cors_url_loader_factory.h b/services/network/cors/cors_url_loader_factory.h
index fe235994..721ec76 100644
--- a/services/network/cors/cors_url_loader_factory.h
+++ b/services/network/cors/cors_url_loader_factory.h
@@ -107,6 +107,9 @@
   const std::optional<url::Origin>& request_initiator_origin_lock() const {
     return request_initiator_origin_lock_;
   }
+  const std::optional<url::Origin>& isolated_world_origin_lock() const {
+    return isolated_world_origin_lock_;
+  }
 
   mojom::CrossOriginEmbedderPolicyReporter* coep_reporter() {
     return coep_reporter_ ? coep_reporter_.get() : nullptr;
@@ -192,6 +195,7 @@
   const OriginatingProcessId process_id_;
   const std::optional<url::Origin> request_initiator_origin_lock_;
   const bool ignore_isolated_world_origin_;
+  const std::optional<url::Origin> isolated_world_origin_lock_;
   const mojom::TrustTokenOperationPolicyVerdict trust_token_issuance_policy_;
   const mojom::TrustTokenOperationPolicyVerdict trust_token_redemption_policy_;
   net::IsolationInfo isolation_info_;
diff --git a/services/network/cors/cors_url_loader_test_util.cc b/services/network/cors/cors_url_loader_test_util.cc
index 20a8b114..6482745 100644
--- a/services/network/cors/cors_url_loader_test_util.cc
+++ b/services/network/cors/cors_url_loader_test_util.cc
@@ -190,6 +190,7 @@
   mojom::URLLoaderFactoryParams params;
   is_trusted = params.is_trusted;
   ignore_isolated_world_origin = params.ignore_isolated_world_origin;
+  isolated_world_origin_lock = params.isolated_world_origin_lock;
   client_security_state = std::move(params.client_security_state);
 
   mojom::URLLoaderFactoryOverride factory_override;
@@ -346,6 +347,8 @@
   factory_params->is_orb_enabled = !process_id.is_browser();
   factory_params->ignore_isolated_world_origin =
       params.ignore_isolated_world_origin;
+  factory_params->isolated_world_origin_lock =
+      params.isolated_world_origin_lock;
   factory_params->factory_override = mojom::URLLoaderFactoryOverride::New();
   factory_params->factory_override->overriding_factory =
       test_url_loader_factory_receiver_->BindNewPipeAndPassRemote();
diff --git a/services/network/cors/cors_url_loader_test_util.h b/services/network/cors/cors_url_loader_test_util.h
index 79006b71..3658d826 100644
--- a/services/network/cors/cors_url_loader_test_util.h
+++ b/services/network/cors/cors_url_loader_test_util.h
@@ -36,6 +36,7 @@
 #include "services/network/resource_scheduler/resource_scheduler.h"
 #include "services/network/test/test_url_loader_network_observer.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "url/origin.h"
 
 class GURL;
 
@@ -201,6 +202,7 @@
     // Members of `mojom::URLLoaderFactoryParams`.
     bool is_trusted;
     bool ignore_isolated_world_origin;
+    std::optional<url::Origin> isolated_world_origin_lock;
     mojom::ClientSecurityStatePtr client_security_state;
 
     // Member of `mojom::URLLoaderFactoryOverride`.
diff --git a/services/network/public/cpp/features.cc b/services/network/public/cpp/features.cc
index 507408c6..e171770 100644
--- a/services/network/public/cpp/features.cc
+++ b/services/network/public/cpp/features.cc
@@ -572,4 +572,6 @@
 BASE_FEATURE(kBindURLLoaderFactoryToHighPriorityTaskRunner,
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+BASE_FEATURE(kEnforceIsolatedWorldOriginLock, base::FEATURE_ENABLED_BY_DEFAULT);
+
 }  // namespace network::features
diff --git a/services/network/public/cpp/features.h b/services/network/public/cpp/features.h
index 9b4fc51..a6ff4a6e 100644
--- a/services/network/public/cpp/features.h
+++ b/services/network/public/cpp/features.h
@@ -389,6 +389,12 @@
 COMPONENT_EXPORT(NETWORK_CPP_FLAGS_AND_SWITCHES)
 BASE_DECLARE_FEATURE(kBindURLLoaderFactoryToHighPriorityTaskRunner);
 
+// When enabled, validates that ResourceRequest::isolated_world_origin matches
+// URLLoaderFactoryParams::isolated_world_origin_lock when
+// ignore_isolated_world_origin is false.
+COMPONENT_EXPORT(NETWORK_CPP_FLAGS_AND_SWITCHES)
+BASE_DECLARE_FEATURE(kEnforceIsolatedWorldOriginLock);
+
 }  // namespace network::features
 
 #endif  // SERVICES_NETWORK_PUBLIC_CPP_FEATURES_H_
diff --git a/services/network/public/mojom/network_context.mojom b/services/network/public/mojom/network_context.mojom
index 048a2ac..1470ee8a 100644
--- a/services/network/public/mojom/network_context.mojom
+++ b/services/network/public/mojom/network_context.mojom
@@ -856,6 +856,17 @@
   // field once Chrome Platform Apps are gone.
   bool ignore_isolated_world_origin = true;
 
+  // The origin of the isolated world that is permitted to use this factory.
+  // When `ignore_isolated_world_origin` is false, requests with an
+  // `isolated_world_origin` that does not match this lock will be rejected
+  // with a bad message report.
+  // Setting this field when `ignore_isolated_world_origin` is true is invalid
+  // and will also result in a bad message report.
+  //
+  // TODO(crbug.com/40137011): Remove `isolated_world_origin_lock` once Chrome
+  // Platform Apps are gone and `ignore_isolated_world_origin` is removed.
+  url.mojom.Origin? isolated_world_origin_lock;
+
   // Indicate whether a request is not from web page ( probably from chrome
   // extension background page ).
   bool unsafe_non_webby_initiator = false;
diff --git a/services/network/cors/cors_url_loader_factory_unittest.cc b/services/network/cors/cors_url_loader_factory_unittest.cc
index ff9554cd..367ab122 100644
--- a/services/network/cors/cors_url_loader_factory_unittest.cc
+++ b/services/network/cors/cors_url_loader_factory_unittest.cc
@@ -190,6 +190,8 @@
     return cors_url_loader_factory_remote_.is_connected();
   }
 
+  OriginAccessList* origin_access_list() { return &origin_access_list_; }
+
  private:
   mojo::FakeMessageDispatchContext mojo_context_;
   // This is required by NetworkBoundCorsURLLoaderFactoryTest but has to live
@@ -795,4 +797,251 @@
 }
 #endif  // BUILDFLAG(ENABLE_DISK_CACHE_SQL_BACKEND)
 
+class IsolatedWorldOriginLockCorsURLLoaderFactoryTest
+    : public CorsURLLoaderFactoryTest {
+ protected:
+  const url::Origin isolated_origin_a_ =
+      url::Origin::Create(GURL("https://isolated-a.example.com"));
+  const url::Origin isolated_origin_b_ =
+      url::Origin::Create(GURL("https://isolated-b.example.com"));
+
+  void SetUp() override {}
+
+  void SetUpFactory(bool ignore_isolated_world_origin,
+                    const std::optional<url::Origin>& lock,
+                    OriginatingProcessId process_id = kProcessId) {
+    auto factory_params = network::mojom::URLLoaderFactoryParams::New();
+    factory_params->ignore_isolated_world_origin = ignore_isolated_world_origin;
+    factory_params->isolated_world_origin_lock = lock;
+    BaseSetup(std::move(factory_params), mojom::NetworkContextParams::New(),
+              process_id);
+  }
+};
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest, MatchingLockAllowed) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/false, isolated_origin_a_);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = isolated_origin_a_;
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_FALSE(bad_message_observer.got_bad_message());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       MismatchedLockRejected) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/false, isolated_origin_a_);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = isolated_origin_b_;
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_TRUE(bad_message_observer.got_bad_message());
+  EXPECT_EQ("CorsURLLoaderFactory: isolated_world_origin lock mismatch",
+            bad_message_observer.WaitForBadMessage());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       MissingLockOnPrivilegedFactoryRejected) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/false, std::nullopt);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = isolated_origin_a_;
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_TRUE(bad_message_observer.got_bad_message());
+  EXPECT_EQ("CorsURLLoaderFactory: isolated_world_origin lock mismatch",
+            bad_message_observer.WaitForBadMessage());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       NoIsolatedWorldOriginAllowed) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/false, isolated_origin_a_);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = std::nullopt;
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_FALSE(bad_message_observer.got_bad_message());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       IgnoredOriginNotRejected) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/true, std::nullopt);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = isolated_origin_b_;
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_FALSE(bad_message_observer.got_bad_message());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       IgnoredOriginClearedAndUnsafeHeadersBlocked) {
+  origin_access_list()->AddAllowListEntryForOrigin(
+      isolated_origin_a_, "http", std::string(test_server()->base_url().host()),
+      /*port=*/0, mojom::CorsDomainMatchMode::kAllowSubdomains,
+      mojom::CorsPortMatchMode::kAllowAnyPort,
+      mojom::CorsOriginAccessMatchPriority::kDefaultPriority);
+
+  // Normal factory ignores isolated world origin and clears it.
+  SetUpFactory(/*ignore_isolated_world_origin=*/true, std::nullopt);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = isolated_origin_a_;
+  request.headers.SetHeader("Sec-Invalid", "value");
+
+  // Since isolated_world_origin is cleared, ShouldAllowUnsafeHeaders falls back
+  // to request_initiator, which does not have allow list privileges, so the
+  // forbidden header causes a bad message.
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_TRUE(bad_message_observer.got_bad_message());
+  EXPECT_EQ("CorsURLLoaderFactory: Forbidden Sec- header from renderer",
+            bad_message_observer.WaitForBadMessage());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest, BrowserProcessAllowed) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/false, isolated_origin_a_,
+               OriginatingProcessId::browser());
+
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/network/cors/cors_url_loader_factory_unittest.cc b/services/network/cors/cors_url_loader_factory_unittest.cc
index ff9554cd..367ab122 100644
--- a/services/network/cors/cors_url_loader_factory_unittest.cc
+++ b/services/network/cors/cors_url_loader_factory_unittest.cc
@@ -190,6 +190,8 @@
     return cors_url_loader_factory_remote_.is_connected();
   }
 
+  OriginAccessList* origin_access_list() { return &origin_access_list_; }
+
  private:
   mojo::FakeMessageDispatchContext mojo_context_;
   // This is required by NetworkBoundCorsURLLoaderFactoryTest but has to live
@@ -795,4 +797,251 @@
 }
 #endif  // BUILDFLAG(ENABLE_DISK_CACHE_SQL_BACKEND)
 
+class IsolatedWorldOriginLockCorsURLLoaderFactoryTest
+    : public CorsURLLoaderFactoryTest {
+ protected:
+  const url::Origin isolated_origin_a_ =
+      url::Origin::Create(GURL("https://isolated-a.example.com"));
+  const url::Origin isolated_origin_b_ =
+      url::Origin::Create(GURL("https://isolated-b.example.com"));
+
+  void SetUp() override {}
+
+  void SetUpFactory(bool ignore_isolated_world_origin,
+                    const std::optional<url::Origin>& lock,
+                    OriginatingProcessId process_id = kProcessId) {
+    auto factory_params = network::mojom::URLLoaderFactoryParams::New();
+    factory_params->ignore_isolated_world_origin = ignore_isolated_world_origin;
+    factory_params->isolated_world_origin_lock = lock;
+    BaseSetup(std::move(factory_params), mojom::NetworkContextParams::New(),
+              process_id);
+  }
+};
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest, MatchingLockAllowed) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/false, isolated_origin_a_);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = isolated_origin_a_;
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_FALSE(bad_message_observer.got_bad_message());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       MismatchedLockRejected) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/false, isolated_origin_a_);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = isolated_origin_b_;
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_TRUE(bad_message_observer.got_bad_message());
+  EXPECT_EQ("CorsURLLoaderFactory: isolated_world_origin lock mismatch",
+            bad_message_observer.WaitForBadMessage());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       MissingLockOnPrivilegedFactoryRejected) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/false, std::nullopt);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = isolated_origin_a_;
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_TRUE(bad_message_observer.got_bad_message());
+  EXPECT_EQ("CorsURLLoaderFactory: isolated_world_origin lock mismatch",
+            bad_message_observer.WaitForBadMessage());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       NoIsolatedWorldOriginAllowed) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/false, isolated_origin_a_);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = std::nullopt;
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_FALSE(bad_message_observer.got_bad_message());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       IgnoredOriginNotRejected) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/true, std::nullopt);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = isolated_origin_b_;
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_FALSE(bad_message_observer.got_bad_message());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       IgnoredOriginClearedAndUnsafeHeadersBlocked) {
+  origin_access_list()->AddAllowListEntryForOrigin(
+      isolated_origin_a_, "http", std::string(test_server()->base_url().host()),
+      /*port=*/0, mojom::CorsDomainMatchMode::kAllowSubdomains,
+      mojom::CorsPortMatchMode::kAllowAnyPort,
+      mojom::CorsOriginAccessMatchPriority::kDefaultPriority);
+
+  // Normal factory ignores isolated world origin and clears it.
+  SetUpFactory(/*ignore_isolated_world_origin=*/true, std::nullopt);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = isolated_origin_a_;
+  request.headers.SetHeader("Sec-Invalid", "value");
+
+  // Since isolated_world_origin is cleared, ShouldAllowUnsafeHeaders falls back
+  // to request_initiator, which does not have allow list privileges, so the
+  // forbidden header causes a bad message.
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_TRUE(bad_message_observer.got_bad_message());
+  EXPECT_EQ("CorsURLLoaderFactory: Forbidden Sec- header from renderer",
+            bad_message_observer.WaitForBadMessage());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest, BrowserProcessAllowed) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/false, isolated_origin_a_,
+               OriginatingProcessId::browser());
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = isolated_origin_b_;
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_FALSE(bad_message_observer.got_bad_message());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       FeatureDisabledBypassesCheck) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitAndDisableFeature(
+      features::kEnforceIsolatedWorldOriginLock);
+
+  SetUpFactory(/*ignore_isolated_world_origin=*/false, isolated_origin_a_);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = isolated_origin_b_;
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_FALSE(bad_message_observer.got_bad_message());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       OpaqueInitiatorWithMatchingIsolatedWorldOriginAllowed) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/false, isolated_origin_a_);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  // An opaque initiator (e.g. a content script running in a sandboxed iframe
+  // or data: URL frame) is allowed by VerifyRequestInitiatorLock, and the
+  // isolated_world_origin matches the factory lock.
+  request.request_initiator = url::Origin();
+  request.isolated_world_origin = isolated_origin_a_;
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_FALSE(bad_message_observer.got_bad_message());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       OpaqueIsolatedWorldOriginOnDefaultFactoryAllowed) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/true, std::nullopt);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  // If an isolated world has an opaque origin, Blink falls back to the default
+  // factory (ignore_isolated_world_origin = true), which clears
+  // isolated_world_origin without reporting a bad message.
+  request.isolated_world_origin = url::Origin();
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_FALSE(bad_message_observer.got_bad_message());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       OpaqueIsolatedWorldOriginOnPrivilegedFactoryRejected) {
+  SetUpFactory(/*ignore_isolated_world_origin=*/false, isolated_origin_a_);
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kInclude;
+  request.method = net::HttpRequestHeaders::kGetMethod;
+  request.url = test_server()->GetURL("/echoall");
+  request.request_initiator = url::Origin::Create(test_server()->base_url());
+  request.isolated_world_origin = url::Origin();
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  CreateLoaderAndStart(request);
+  EXPECT_TRUE(bad_message_observer.got_bad_message());
+  EXPECT_EQ("CorsURLLoaderFactory: isolated_world_origin lock mismatch",
+            bad_message_observer.WaitForBadMessage());
+}
+
+TEST_F(IsolatedWorldOriginLockCorsURLLoaderFactoryTest,
+       LockPresentWhenIgnoredOriginIsTrueRejected) {
+  mojo::test::BadMessageObserver bad_message_observer;
+  SetUpFactory(/*ignore_isolated_world_origin=*/true, isolated_origin_a_);
+  EXPECT_TRUE(bad_message_observer.got_bad_message());
+  EXPECT_EQ(
+      "CorsURLLoaderFactory: isolated_world_origin_lock set when "
+      "ignore_isolated_world_origin is true",
+      bad_message_observer.WaitForBadMessage());
+}
+
 }  // namespace network::cors
diff --git a/services/network/cors/cors_url_loader_unittest.cc b/services/network/cors/cors_url_loader_unittest.cc
index 764606e3..127d6ec8 100644
--- a/services/network/cors/cors_url_loader_unittest.cc
+++ b/services/network/cors/cors_url_loader_unittest.cc
@@ -583,6 +583,7 @@
 
   ResetFactoryParams factory_params;
   factory_params.ignore_isolated_world_origin = false;
+  factory_params.isolated_world_origin_lock = extension_origin;
   ResetFactory(url::Origin::Create(origin), kRendererProcessId, factory_params);
 
   AddAllowListEntryForOrigin(extension_origin, std::string(url.scheme()),
@@ -1897,6 +1898,7 @@
 
   ResetFactoryParams factory_params;
   factory_params.ignore_isolated_world_origin = false;
+  factory_params.isolated_world_origin_lock = isolated_world_origin;
   ResetFactory(main_world_origin, kRendererProcessId, factory_params);
 
   AddAllowListEntryForOrigin(isolated_world_origin, url.GetScheme(),
@@ -1942,6 +1944,7 @@
 
   ResetFactoryParams factory_params;
   factory_params.ignore_isolated_world_origin = false;
+  factory_params.isolated_world_origin_lock = isolated_world_origin;
   ResetFactory(main_world_origin, kRendererProcessId, factory_params);
 
   AddAllowListEntryForOrigin(isolated_world_origin, url.GetScheme(),
Loading diff…