Chrome · Preload
CVE-2026-79264
Logic Error in Preload
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchthird_party/blink/renderer/platform/loader/fetch/resource.cc |
modified | |
TEST_Pthird_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.cc |
modified |
Files Changed
third_party/blink/renderer/platform/loader/fetch/resource.ccthird_party/blink/renderer/platform/loader/fetch/resource.hthird_party/blink/renderer/platform/loader/fetch/resource_fetcher.ccthird_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.ccthird_party/blink/renderer/platform/loader/fetch/resource_test.cc
Patch
From c4ae60d6090f10c5de4d19938e02ba840586abbc Mon Sep 17 00:00:00 2001 From: Justin Lulejian <[email protected]> Date: Thu, 09 Jul 2026 08:51:58 -0700 Subject: [PATCH] [Extensions] Prevent cross-world resource reuse via Service Worker Before this change, a resource fetched via a Service Worker in one script world could be cached in Blink's MemoryCache and later reused by a different script world (like an isolated world for an extension). This could lead to unexpected code execution in the privileged context if the Service Worker had modified the resource. After this change, resources fetched via a Service Worker are only reused if the requesting script world matches the world that fetched the resource. This ensures that Service Worker responses are not unnecessarily shared across different script worlds (Extensions, DevTools, etc.). We accomplished this by modifying Resource::CanReuse to check if the resource was fetched via a Service Worker, and if so, verifying that the world_for_csp of the cached resource matches the world_for_csp of the new request. We also added a unit test to verify this behavior and updated comments to explain the necessity of both this check and the extension-specific check. TAG=agy CONV=83e8b1e4-14c6-4be8-9843-c914864548fa Fixed: 507483993 Change-Id: I642118ef71b90b49fb744b58805f27cbbc2d29e6 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8007646 Auto-Submit: Justin Lulejian <[email protected]> Reviewed-by: Yoshisato Yanagisawa <[email protected]> Commit-Queue: Justin Lulejian <[email protected]> Cr-Commit-Position: refs/heads/main@{#1659592} --- diff --git a/third_party/blink/renderer/platform/loader/fetch/resource.cc b/third_party/blink/renderer/platform/loader/fetch/resource.cc index 0b0ea72..9fb33c6e 100644 --- a/third_party/blink/renderer/platform/loader/fetch/resource.cc +++ b/third_party/blink/renderer/platform/loader/fetch/resource.cc @@ -71,12 +71,20 @@ #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" #include "third_party/blink/renderer/platform/wtf/vector.h" -// Feature that prevents an extension resource from being fetched across -// isolated worlds. +namespace blink { + +// TODO(crbug.com/507483993): Enable these behaviors by default and remove the +// feature flags after monitoring for regressions. + +// Feature that prevents an extension resource (chrome-extension://...) from +// being fetched across isolated worlds. BASE_FEATURE(kPreventExtensionResourceFetchAcrossIsolatedWorlds, base::FEATURE_ENABLED_BY_DEFAULT); -namespace blink { +// Feature that prevents resources fetched via a Service Worker from being +// reused across different script worlds. +BASE_FEATURE(kPreventCrossWorldServiceWorkerResourceReuse, + base::FEATURE_ENABLED_BY_DEFAULT); String GetAsAttributeFromResourceType(ResourceType type) { switch (type) { @@ -866,7 +874,17 @@ // Use GetResourceRequest to get the const resource_request_. const ResourceRequestHead& current_request = GetResourceRequest(); - // Extensions resources should not fetch across isolated worlds. + // We need two distinct checks here to prevent unexpected cross-world + // resource reuse. + // + // 1. The extension-specific check prevents sharing of extension + // resources (chrome-extension://...) across different script worlds, + // even for standard network loads. + // For example, if a main world page preloads a web-accessible extension + // resource, reusing that cached resource in the extension's isolated + // world could bypass world-specific loader checks. + // This behavior is tested in + // `ResourceFetcherTest.CrossWorldExtensionResourceMismatch`. if (base::FeatureList::IsEnabled( kPreventExtensionResourceFetchAcrossIsolatedWorlds) && CommonSchemeRegistry::IsExtensionScheme( @@ -875,6 +893,20 @@ return MatchStatus::kCrossWorldExtensionResourceMismatch; } + // 2. The Service Worker check prevents sharing of any resource that was + // fetched via a Service Worker across different script worlds. This is + // necessary because a Service Worker in one world (e.g., the main world) + // could modify the response of a resource that is later loaded by an + // isolated world (e.g., an extension, DevTools, or a userscript), leading + // to unexpected code execution in that world. + // This behavior is tested in `ResourceTest.CanReuseServiceWorkerResource`. + if (base::FeatureList::IsEnabled( + kPreventCrossWorldServiceWorkerResourceReuse) && + GetResponse().WasFetchedViaServiceWorker() && + options_.world_for_csp != new_options.world_for_csp) { + return MatchStatus::kCrossWorldServiceWorkerResourceMismatch; + } + // If credentials mode is different from the the previous request, re-fetch // the resource. // diff --git a/third_party/blink/renderer/platform/loader/fetch/resource.h b/third_party/blink/renderer/platform/loader/fetch/resource.h index c21b7e1..a6abed96 100644 --- a/third_party/blink/renderer/platform/loader/fetch/resource.h +++ b/third_party/blink/renderer/platform/loader/fetch/resource.h @@ -162,6 +162,9 @@ // Match fails because it's a cross-world extension resource request. kCrossWorldExtensionResourceMismatch, + + // Match fails because it's a cross-world service worker resource request. + kCrossWorldServiceWorkerResourceMismatch, }; Resource(const Resource&) = delete; diff --git a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc index 8a487243..8bbb5620 100644 --- a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc +++ b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc @@ -1971,6 +1971,10 @@ builder.Append( "because it is a cross-world extension resource mismatch."); break; + case Resource::MatchStatus::kCrossWorldServiceWorkerResourceMismatch: + builder.Append( + "because it is a cross-world service worker resource mismatch."); + break; } console_logger_->AddConsoleMessage(mojom::ConsoleMessageSource::kOther, mojom::ConsoleMessageLevel::kWarning, diff --git a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.cc b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.cc index 090d997..35ff0ee 100644 --- a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.cc +++ b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.cc @@ -2443,4 +2443,56 @@ CommonSchemeRegistry::RemoveURLSchemeAsExtensionForTest("chrome-extension"); } +// Tests that a preloaded resource fetched via a Service Worker is not reused +// by a request from a different script world, even if the URL and resource +// type match. +// +// Practical Example: +// A main world page preloads a script from a CDN +// (e.g., `https://cdn.com/lib.js`) which is served with a custom response by +// the page's Service Worker. Later, an extension content script (isolated +// world) attempts to fetch the same CDN script. Rather than fetch and reuse +// the Service Worker cached script, the extension should perform a fresh fetch +// to obtain the resource. +TEST_P(ResourceFetcherTest, PreloadMatchServiceWorkerWorldMismatch) { + auto* fetcher = CreateFetcher(); + KURL url("http://127.0.0.1:8000/foo.js"); + + // Register mock response with `SetWasFetchedViaServiceWorker(true)`. + ResourceResponse response(url); + response.SetHttpStatusCode(200); + response.SetWasFetchedViaServiceWorker(true); + platform_->GetURLLoaderMockFactory()->RegisterURL( + url, WrappedResourceResponse(response), + test::PlatformTestDataPath(kTestResourceFilename)); + + // 1. Trigger Preload in main world (default world_for_csp is null) + FetchParameters fetch_params_preload = + FetchParameters::CreateForTest(ResourceRequest(url)); + fetch_params_preload.SetLinkPreload(true); + Resource* preload_resource = + MockResource::Fetch(fetch_params_preload, fetcher, nullptr); + ASSERT_TRUE(preload_resource); + EXPECT_TRUE(preload_resource->IsLinkPreload()); + platform_->GetURLLoaderMockFactory()->ServeAsynchronousRequests(); + EXPECT_TRUE(preload_resource->IsLoaded()); + + // 2. Fetch in isolated world (different world_for_csp) + FetchParameters fetch_params_load = + FetchParameters::CreateForTest(ResourceRequest(url)); + DOMWrapperWorld* isolated_world = DOMWrapperWorld::EnsureIsolatedWorld( + /*v8::Isolate=*/nullptr, blink::kIsolatedWorldIdLimit - 1); + fetch_params_load.MutableOptions().world_for_csp = isolated_world; + + // Verify that the loader detects the script world mismatch for the + // Service Worker-fetched resource (returning + // `kCrossWorldServiceWorkerResourceMismatch`). The preload should be + // rejected for reuse, forcing a new load to start and returning a different + // resource instance. + Resource* load_resource = + MockResource::Fetch(fetch_params_load, fetcher, nullptr); + + EXPECT_NE(preload_resource, load_resource); +} + } // namespace blink diff --git a/third_party/blink/renderer/platform/loader/fetch/resource_test.cc b/third_party/blink/renderer/platform/loader/fetch/resource_test.cc index 1ae9fde2..d7a75004d 100644 --- a/third_party/blink/renderer/platform/loader/fetch/resource_test.cc +++ b/third_party/blink/renderer/platform/loader/fetch/resource_test.cc @@ -8,7 +8,9 @@
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.cc b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.cc
index 090d997..35ff0ee 100644
--- a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.cc
+++ b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.cc
@@ -2443,4 +2443,56 @@
CommonSchemeRegistry::RemoveURLSchemeAsExtensionForTest("chrome-extension");
}
+// Tests that a preloaded resource fetched via a Service Worker is not reused
+// by a request from a different script world, even if the URL and resource
+// type match.
+//
+// Practical Example:
+// A main world page preloads a script from a CDN
+// (e.g., `https://cdn.com/lib.js`) which is served with a custom response by
+// the page's Service Worker. Later, an extension content script (isolated
+// world) attempts to fetch the same CDN script. Rather than fetch and reuse
+// the Service Worker cached script, the extension should perform a fresh fetch
+// to obtain the resource.
+TEST_P(ResourceFetcherTest, PreloadMatchServiceWorkerWorldMismatch) {
+ auto* fetcher = CreateFetcher();
+ KURL url("http://127.0.0.1:8000/foo.js");
+
+ // Register mock response with `SetWasFetchedViaServiceWorker(true)`.
+ ResourceResponse response(url);
+ response.SetHttpStatusCode(200);
+ response.SetWasFetchedViaServiceWorker(true);
+ platform_->GetURLLoaderMockFactory()->RegisterURL(
+ url, WrappedResourceResponse(response),
+ test::PlatformTestDataPath(kTestResourceFilename));
+
+ // 1. Trigger Preload in main world (default world_for_csp is null)
+ FetchParameters fetch_params_preload =
+ FetchParameters::CreateForTest(ResourceRequest(url));
+ fetch_params_preload.SetLinkPreload(true);
+ Resource* preload_resource =
+ MockResource::Fetch(fetch_params_preload, fetcher, nullptr);
+ ASSERT_TRUE(preload_resource);
+ EXPECT_TRUE(preload_resource->IsLinkPreload());
+ platform_->GetURLLoaderMockFactory()->ServeAsynchronousRequests();
+ EXPECT_TRUE(preload_resource->IsLoaded());
+
+ // 2. Fetch in isolated world (different world_for_csp)
+ FetchParameters fetch_params_load =
+ FetchParameters::CreateForTest(ResourceRequest(url));
+ DOMWrapperWorld* isolated_world = DOMWrapperWorld::EnsureIsolatedWorld(
+ /*v8::Isolate=*/nullptr, blink::kIsolatedWorldIdLimit - 1);
+ fetch_params_load.MutableOptions().world_for_csp = isolated_world;
+
+ // Verify that the loader detects the script world mismatch for the
+ // Service Worker-fetched resource (returning
+ // `kCrossWorldServiceWorkerResourceMismatch`). The preload should be
+ // rejected for reuse, forcing a new load to start and returning a different
+ // resource instance.
+ Resource* load_resource =
+ MockResource::Fetch(fetch_params_load, fetcher, nullptr);
+
+ EXPECT_NE(preload_resource, load_resource);
+}
+
} // namespace blink
diff --git a/third_party/blink/renderer/platform/loader/fetch/resource_test.cc b/third_party/blink/renderer/platform/loader/fetch/resource_test.cc
index 1ae9fde2..d7a75004d 100644
--- a/third_party/blink/renderer/platform/loader/fetch/resource_test.cc
+++ b/third_party/blink/renderer/platform/loader/fetch/resource_test.cc
@@ -8,7 +8,9 @@
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/platform.h"
+#include "third_party/blink/renderer/platform/bindings/dom_wrapper_world.h"
#include "third_party/blink/renderer/platform/heap/thread_state.h"
+#include "third_party/blink/renderer/platform/loader/fetch/fetch_parameters.h"
#include "third_party/blink/renderer/platform/loader/fetch/memory_cache.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_request.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_response.h"
@@ -16,6 +18,7 @@
#include "third_party/blink/renderer/platform/loader/testing/mock_resource_client.h"
#include "third_party/blink/renderer/platform/scheduler/test/task_environment.h"
#include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
+#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/shared_buffer.h"
namespace blink {
@@ -559,4 +562,48 @@
EXPECT_FALSE(weak_resource);
}
+// Tests that resources fetched via a Service Worker are only reused if the
+// requesting world matches the world that fetched the resource. This prevents
+// unexpected cross-world resource reuse.
+TEST_F(ResourceTest, CanReuseServiceWorkerResource) {
+ KURL url("http://127.0.0.1:8000/foo.html");
+ scoped_refptr<const SecurityOrigin> origin = SecurityOrigin::Create(url);
+
+ // Set up the cached resource which was fetched via a Service Worker.
+ ResourceRequest cache_request(url);
+ cache_request.SetRequestorOrigin(origin);
+
+ ResourceResponse response(url);
+ response.SetHttpStatusCode(200);
+ response.SetWasFetchedViaServiceWorker(true);
+
+ auto* resource = MakeGarbageCollected<MockResource>(cache_request);
+ resource->ResponseReceived(response);
+ resource->FinishForTest();
+
+ // Verify that a request from the same world (both have null `world_for_csp`
+ // which represents the main world) can reuse the cached resource.
+ {
+ ResourceRequest request(url);
+ request.SetRequestorOrigin(origin);
+ FetchParameters params = FetchParameters::CreateForTest(std::move(request));
+ EXPECT_EQ(Resource::MatchStatus::kOk, resource->CanReuse(params));
+ }
+
+ // Verify that a request from a different isolated world (e.g., an extension
+ // or devtools) cannot reuse the cached resource, returning
+ // `Resource::MatchStatus::kCrossWorldServiceWorkerResourceMismatch`.
+ {
+ ResourceRequest request(url);
+ request.SetRequestorOrigin(origin);
+ FetchParameters params = FetchParameters::CreateForTest(std::move(request));
+ DOMWrapperWorld* isolated_world = DOMWrapperWorld::EnsureIsolatedWorld(
+ /*v8::Isolate=*/nullptr, blink::kIsolatedWorldIdLimit - 1);
+ params.MutableOptions().world_for_csp = isolated_world;
+
+ EXPECT_EQ(Resource::MatchStatus::kCrossWorldServiceWorkerResourceMismatch,
+ resource->CanReuse(params));
+ }
+}
+
} // namespace blink
Loading diff…
Original Bug Report
reported by [email protected]
Context Isolation Bypass via SW-Poisoned MemoryCache
A vulnerability in Blink’s MemoryCache allows a malicious web page to bypass context isolation by poisoning resources used by isolated worlds (e.g., Extensions, DevTools, Userscripts). I’m filing this to get agreement on the best future-proofing solution to this exploit.
Exploit steps example (for an extension)
- Trigger Preload: A malicious web (non-extension) page preloads a sensitive resource (e.g., chrome-extension://[ID]/script.js).
- Interception: The page’s Service Worker (SW) intercepts the preload and returns malicious content via event.respondWith().
- Caching: Blink’s MemoryCache stores this poisoned response, keyed by URL but associated with the main world.
- Execution: When an extension later attempts to load the same URL in its isolated world, the MemoryCache reuses the poisoned resource, executing the attacker’s code in the extension’s privileged context.
Compare solutions
I have a limited to extensions fix in CL 7796808, but I think there’s a potential to fix the root cause by preventing cross-world resource loads from SW cached resources (below).
| Feature | Targeted Fix (CL 7796808) | Comprehensive Fix (Proposed) |
|---|---|---|
| Scope | chrome-extension:// only | All SW-intercepted resources |
| Protection | Blocks extension script poisoning | Blocks poisoning of scripts, JSON, and data across all schemes (e.g., https://) |
| Consistency | Scheme-specific workaround | Aligns with the network stack (which already bypasses page SW for isolated worlds) |
Potential comprehensive fix
Modify Resource::CanReuse in third_party/blink/renderer/platform/loader/fetch/resource.cc to enforce world-partitioning for all Service Worker responses:
bool Resource::CanReuse(const FetchParameters& params) const {
// ... existing reuse checks ...
// If fulfilled by a Service Worker, the resource is only safe to reuse
// within the same script world. This prevents a main-world SW from
// poisoning resources used by isolated worlds.
if (GetResponse().WasFetchedViaServiceWorker() &&
options_.world_for_csp != params.Options().world_for_csp) {
return false;
}
return true;
}
Potential concerns
- Standard web browsing should remain unaffected (web page scripts in the main world will continue to use the cache).
- AFAIK this shouldn’t impact worker and main world communication since workers are on a separate thread from main and do not reuse the main world cache.
- If an web page and an extension wanted to share a common library this will break them. However, extensions are, by policy, not allowed to load remotely hosted code so this breakage is not a concern.
References
On This Page