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 <[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)
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(),