CVE-2026-79264
Overview
Background
- Blink `MemoryCache` / preload reuse
- Blink caches fetched
Resourceobjects and, when a later request matches, reuses the cached bytes instead of re-fetching over the network. - Script world (`world_for_csp`)
- A
DOMWrapperWorldidentifying the execution context of a request; the main world uses a nullworld_for_csp, while extension content scripts, DevTools, and userscripts run in isolated worlds with distinct values. - Service Worker interception
- A Service Worker registered by a page can intercept a fetch and synthesize an arbitrary response, so a resource marked
WasFetchedViaServiceWorker()carries content the page’s SW chose rather than the origin server’s bytes. - `Resource::CanReuse`
- The predicate that decides whether a cached
Resourcemay satisfy a newFetchParametersrequest, returning aMatchStatussuch askOkor a mismatch reason.
Root Cause Analysis
Before the patch, Resource::CanReuse in resource.cc only rejected cross-world reuse for extension-scheme resources (chrome-extension://...) via the kPreventExtensionResourceFetchAcrossIsolatedWorlds check; it had no analogous check for resources whose bytes came from a Service Worker. Consequently a resource that a main-world page preloaded and whose response was synthesized by that page’s Service Worker could be cached in the MemoryCache and later reused to satisfy a request originating in a different script world (an extension isolated world, DevTools, or a userscript), because the URL and resource type matched. The invariant violated is that a Service-Worker-synthesized response—effectively attacker-influenced content—must not silently cross the world boundary into a more privileged context that would otherwise perform its own loader/CSP checks on a fresh fetch.
The fix adds a second check: when kPreventCrossWorldServiceWorkerResourceReuse is enabled, the cached resource’s GetResponse().WasFetchedViaServiceWorker() is true, and options_.world_for_csp != new_options.world_for_csp, CanReuse returns the new kCrossWorldServiceWorkerResourceMismatch, forcing a fresh load in the requesting world. This tightens reuse eligibility, so same-world reuse still returns kOk while cross-world reuse is refused.
CanReuse compare the fetching and requesting world_for_csp for any SW-fetched resource and refuse reuse when they differ.Attack Path
- Register a Service Worker that rewrites a response
An attacker-controlled page registers a Service Worker that intercepts a fetch (e.g.
https://cdn.com/lib.js) and returns a custom, attacker-chosen script body. - Preload into the main world
The page triggers a main-world preload of that URL, causing the SW-synthesized
Resource(withWasFetchedViaServiceWorker()true and a nullworld_for_csp) to be cached in theMemoryCache. - Trigger a fetch from another world Code in a different script world—an extension content script’s isolated world, DevTools, or a userscript—requests the same URL and resource type.
- Cross-world reuse of tainted bytes
Pre-patch,
Resource::CanReusereturnedkOk, so the isolated world received the Service-Worker-modified script instead of performing its own fresh, world-checked load. - Execution in the privileged context (inferred)
The reused, attacker-influenced script could then run in the more privileged world, bypassing world-specific loader checks; the concrete privileged-code-execution outcome is
(inferred)beyond what the patch demonstrates.
Impact Assessment
LogicError / “Incorrect reference resolution in Preload” in the Preload component; unexpected code execution in the privileged context is described as a possibility and is marked (inferred) here. The precondition is the ability to register a Service Worker and induce a same-URL fetch from a different world; the advisory does not state exploitation in the wild.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 | |
TEST_Fthird_party/blink/renderer/platform/loader/fetch/resource_test.cc |
modified |
Files Changed
third_party/blink/renderer/platform/loader/fetch/resource_fetcher_test.ccthird_party/blink/renderer/platform/loader/fetch/resource.ccthird_party/blink/renderer/platform/loader/fetch/resource_test.ccthird_party/blink/renderer/platform/loader/fetch/resource_fetcher.ccthird_party/blink/renderer/platform/loader/fetch/resource.h
Audit Directions
- Cache-reuse predicates ignoring provenanceReview every branch of
Resource::CanReuseand sibling cache-match logic for reuse decisions that key only on URL/type while ignoring how the response was produced (Service Worker, CORS mode, credentials, world). - World-boundary crossings for cached dataAudit other
MemoryCache/preload consumers whereworld_for_csporDOMWrapperWorlddistinctions must gate sharing between main and isolated worlds (extensions, DevTools, userscripts). - Feature-flag-gated security checksTrace new checks guarded by flags like
kPreventCrossWorldServiceWorkerResourceReuseto confirm the flag is enabled-by-default and that no alternate creation path bypasses the guard.
Patch
From c4ae60d6090f10c5de4d19938e02ba840586abbc Mon Sep 17 00:00:00 2001
From: Justin Lulejian <jlulejian@chromium.org>
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 <jlulejian@chromium.org>
Reviewed-by: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Commit-Queue: Justin Lulejian <jlulejian@chromium.org>
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 @@
#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
Regression Test / PoC
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
Original Bug Report
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.