CVE-2026-102320
Overview
Background
- `CorsURLLoaderFactory`
- the network-service object that validates each
ResourceRequestfrom a renderer (viaIsValidRequest) before creating aCorsURLLoader, enforcing per-factory origin locks. - `request_initiator_origin_lock_`
- the origin a factory is bound to, against which
request.request_initiatoris already validated; the pre-patch code enforced this but had no equivalent lock for isolated-world origins. - `isolated_world_origin`
- a
ResourceRequestfield naming the extension/content-script (isolated world) origin, used by downstream checks such asShouldAllowUnsafeHeadersto grant extension privileges (CORS/ORB relaxations,Sec-headers). - `ignore_isolated_world_origin`
- a factory flag that is
truefor normal web-page factories (extension origins are meaningless) andfalsefor 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.
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
- Obtain a privileged factory
A compromised renderer that legitimately holds an isolated-world
CorsURLLoaderFactory(ignore_isolated_world_origin_false) sends aResourceRequestthrough it. (inferred that the renderer is compromised, per the advisory’s threat model.) - Spoof the isolated-world origin
The renderer sets
request.isolated_world_originto an arbitrary extension origin it does not actually own; pre-patchIsValidRequestperforms no check against any lock. - Downstream trust granted
Consumers such as
ShouldAllowUnsafeHeadersaccept the spoofed origin and grant its allow-list privileges, permitting forbiddenSec-headers or relaxed CORS/ORB handling. - 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
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
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/url_loader_factory_params_helper.cc |
modified | |
ifservices/network/cors/cors_url_loader_factory.cc |
modified | |
GURLservices/network/cors/cors_url_loader_test_util.h |
modified | |
IsolatedWorldOriginLockCorsURLLoaderFactoryTestservices/network/cors/cors_url_loader_factory_unittest.cc |
modified | |
TEST_Fservices/network/cors/cors_url_loader_factory_unittest.cc |
modified |
Files Changed
services/network/cors/cors_url_loader_test_util.hservices/network/public/cpp/features.hservices/network/cors/cors_url_loader_factory.hservices/network/public/mojom/network_context.mojomservices/network/cors/cors_url_loader_factory.ccservices/network/cors/cors_url_loader_unittest.cccontent/browser/url_loader_factory_params_helper.ccservices/network/cors/cors_url_loader_test_util.ccservices/network/cors/cors_url_loader_factory_unittest.ccservices/network/public/cpp/features.cc
Audit Directions
- Trust-bearing request fields without a factory lockEnumerate every
ResourceRequestfield 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 fieldsWhere 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 consumersAudit
ShouldAllowUnsafeHeadersand 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.
Patch
From 22d40030760cb88140206e0b5573eee00e1b13fb Mon Sep 17 00:00:00 2001
From: Takashi Toyoshima <toyoshim@chromium.org>
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 <bashi@chromium.org>
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Commit-Queue: Takashi Toyoshima <toyoshim@chromium.org>
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)
Regression Test / PoC
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(),