CVE-2026-11197
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/chrome_content_browser_client.cc |
modified | |
TEST_Fcontent/browser/worker_host/shared_worker_service_impl_unittest.cc |
modified |
Files Changed
chrome/browser/chrome_content_browser_client.ccchrome/browser/chrome_content_browser_client.hcontent/browser/bad_message.hcontent/browser/worker_host/dedicated_worker_host.cccontent/browser/worker_host/shared_worker_service_impl.cccontent/browser/worker_host/shared_worker_service_impl_unittest.cc
Patch
From 0e9234d7c0a1074df286f33eae9c1008a5d05a6e Mon Sep 17 00:00:00 2001 From: Yoshisto Yanagisawa <[email protected]> Date: Sun, 26 Apr 2026 20:48:25 -0700 Subject: [PATCH] SharedWorker: Enforce same-origin check for IWA and Extensions This is a security hardening to prevent asset exfiltration from extension and Isolated Web App (IWA) contexts by compromised renderer processes. A compromised renderer hosting a chrome-extension:// or isolated-app:// document could previously construct a SharedWorker whose main-script URL was cross-origin to the creator. This was because the same-origin check in SharedWorkerServiceImpl::ConnectToWorker allowed cross-origin requests if the scheme was allowlisted (and chrome-extension was whitelisted in ChromeContentBrowserClient::DoesSchemeAllowCrossOriginSharedWorker). This CL adds a strict same-origin check for these schemes in SharedWorkerServiceImpl::ConnectToWorker, gated by a new feature flag `kEnforceSharedWorkerSameOriginCheck`. If a violation is detected, the browser process terminates the renderer with a bad message (`SWSI_CROSS_ORIGIN_SCRIPT_URL`). This is a sibling fix to the DedicatedWorker hardening landed in commit 8bde565f45a8baf9b84c129905eb53c9abe108d2. This CL also updates `RegisterNonNetworkWorkerMainResourceURLLoaderFactories` to take `RequestDestination` to strictly separate the behavior for DedicatedWorker and SharedWorker when creating `IsolatedWebAppURLLoaderFactory`. Bug: 504073872 Change-Id: I852098d5ffc7b31d176de87dc76bc81dc429636f Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7784632 Reviewed-by: Rakina Zata Amni <[email protected]> Reviewed-by: Steven Holte <[email protected]> Commit-Queue: Yoshisato Yanagisawa <[email protected]> Reviewed-by: Andrea Orru <[email protected]> Cr-Commit-Position: refs/heads/main@{#1620886} --- diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc index 08c706a8..61e6a40 100644 --- a/chrome/browser/chrome_content_browser_client.cc +++ b/chrome/browser/chrome_content_browser_client.cc @@ -403,6 +403,7 @@ #include "services/network/public/cpp/self_deleting_url_loader_factory.h" #include "services/network/public/cpp/web_sandbox_flags.h" #include "services/network/public/mojom/cert_verifier_service.mojom.h" +#include "services/network/public/mojom/fetch_api.mojom.h" #include "services/network/public/mojom/network_service.mojom.h" #include "services/network/public/mojom/url_loader_factory.mojom.h" #include "services/network/public/mojom/web_transport.mojom.h" @@ -6142,6 +6143,7 @@ RegisterNonNetworkWorkerMainResourceURLLoaderFactories( content::BrowserContext* browser_context, const std::optional<url::Origin>& request_initiator, + network::mojom::RequestDestination request_destination, NonNetworkURLLoaderFactoryMap* factories) { DCHECK(browser_context); DCHECK(factories); @@ -6159,12 +6161,18 @@ request_initiator->scheme() == webapps::kIsolatedAppScheme) { app_origin = request_initiator; } - factories->emplace( - webapps::kIsolatedAppScheme, - web_app::IsolatedWebAppURLLoaderFactory::Create( - browser_context, app_origin, - base::FeatureList::IsEnabled( - features::kEnforceDedicatedWorkerSameOriginCheck))); + bool enforce_same_origin = false; + if (request_destination == network::mojom::RequestDestination::kWorker) { + enforce_same_origin = base::FeatureList::IsEnabled( + features::kEnforceDedicatedWorkerSameOriginCheck); + } else if (request_destination == + network::mojom::RequestDestination::kSharedWorker) { + enforce_same_origin = base::FeatureList::IsEnabled( + features::kEnforceSharedWorkerSameOriginCheck); + } + factories->emplace(webapps::kIsolatedAppScheme, + web_app::IsolatedWebAppURLLoaderFactory::Create( + browser_context, app_origin, enforce_same_origin)); } #endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || // BUILDFLAG(IS_CHROMEOS) diff --git a/chrome/browser/chrome_content_browser_client.h b/chrome/browser/chrome_content_browser_client.h index 3af70c2..d4f99635 100644 --- a/chrome/browser/chrome_content_browser_client.h +++ b/chrome/browser/chrome_content_browser_client.h @@ -686,6 +686,7 @@ void RegisterNonNetworkWorkerMainResourceURLLoaderFactories( content::BrowserContext* browser_context, const std::optional<url::Origin>& request_initiator, + network::mojom::RequestDestination request_destination, NonNetworkURLLoaderFactoryMap* factories) override; void RegisterNonNetworkServiceWorkerUpdateURLLoaderFactories( content::BrowserContext* browser_context, diff --git a/content/browser/bad_message.h b/content/browser/bad_message.h index d1b63fb..69ef4deb 100644 --- a/content/browser/bad_message.h +++ b/content/browser/bad_message.h @@ -366,6 +366,7 @@ RFH_ENTER_FULLSCREEN_PERMISSION_DENIED = 338, DT_DUPLICATE_CHILD_TARGET_CREATED = 339, RWH_POINTER_LOCK_FROM_SANDBOXED_FRAME = 340, + SWSI_CROSS_ORIGIN_SCRIPT_URL = 341, // Please add new elements here. The naming convention is abbreviated class // name (e.g. RenderFrameHost becomes RFH) plus a unique description of the diff --git a/content/browser/worker_host/dedicated_worker_host.cc b/content/browser/worker_host/dedicated_worker_host.cc index 8a358f5..2983ec86 100644 --- a/content/browser/worker_host/dedicated_worker_host.cc +++ b/content/browser/worker_host/dedicated_worker_host.cc @@ -1083,7 +1083,7 @@ worker_process_host_->GetDeprecatedID(), storage_partition_impl, partition_domain, file_url_support_, /*filesystem_url_support=*/true, creator_render_frame_host, - worker_storage_key_); + worker_storage_key_, network::mojom::RequestDestination::kWorker); bool bypass_redirect_checks = false; subresource_loader_factories->pending_default_factory() = diff --git a/content/browser/worker_host/shared_worker_service_impl.cc b/content/browser/worker_host/shared_worker_service_impl.cc index 5cfdc3c..c5cc4b1f 100644 --- a/content/browser/worker_host/shared_worker_service_impl.cc +++ b/content/browser/worker_host/shared_worker_service_impl.cc @@ -37,7 +37,9 @@ #include "content/public/browser/shared_worker_instance.h" #include "content/public/browser/site_isolation_policy.h" #include "content/public/common/content_client.h" +#include "content/public/common/content_features.h" #include "content/public/common/content_switches.h" +#include "mojo/public/cpp/bindings/message.h" #include "mojo/public/cpp/bindings/remote.h" #include "net/base/isolation_info.h" #include "net/cookies/site_for_cookies.h" @@ -165,6 +167,31 @@ const blink::StorageKey& storage_key = storage_key_override.value_or(render_frame_host->GetStorageKey()); + if (base::FeatureList::IsEnabled( + features::kEnforceSharedWorkerSameOriginCheck) && + !info->url.SchemeIs(url::kDataScheme)) { + url::Origin script_origin = url::Origin::Create(info->url); + if (storage_key.origin() != script_origin) { + if (storage_key.origin().opaque() && + storage_key.origin().GetTupleOrPrecursorTupleIfOpaque() == + script_origin.GetTupleOrPrecursorTupleIfOpaque()) { + // Match found via precursor. + } else { + constexpr char kIsolatedAppScheme[] = "isolated-app"; + constexpr char kExtensionScheme[] = "chrome-extension"; + if (storage_key.origin().scheme() == kIsolatedAppScheme || + storage_key.origin().scheme() == kExtensionScheme || + script_origin.scheme() == kIsolatedAppScheme || + script_origin.scheme() == kExtensionScheme) { + bad_message::ReceivedBadMessage( + render_frame_host->GetProcess(), + bad_message::SWSI_CROSS_ORIGIN_SCRIPT_URL); + return; + } + } + } + } + // Enforce same-origin policy. // data: URLs are not considered a different origin. bool is_cross_origin = !info->url.SchemeIs(url::kDataScheme) && diff --git a/content/browser/worker_host/shared_worker_service_impl_unittest.cc b/content/browser/worker_host/shared_worker_service_impl_unittest.cc index fea3d437..3d56bd7 100644 --- a/content/browser/worker_host/shared_worker_service_impl_unittest.cc +++ b/content/browser/worker_host/shared_worker_service_impl_unittest.cc @@ -29,6 +29,7 @@ #include "content/public/browser/browser_context.h" #include "content/public/browser/storage_partition.h" #include "content/public/test/mock_render_process_host.h" +#include "content/public/test/navigation_simulator.h" #include "content/public/test/test_browser_context.h" #include "content/public/test/test_utils.h" #include "content/test/fake_network_url_loader_factory.h" @@ -45,6 +46,7 @@ #include "third_party/blink/public/common/messaging/message_port_channel.h" #include "third_party/blink/public/mojom/worker/shared_worker_info.mojom.h" #include "url/origin.h" +#include "url/url_util.h" using blink::MessagePortChannel; @@ -1833,4 +1835,196 @@ ContextTypeTestCase::kMismatchRendererSecure, ContextTypeTestCase::kMismatchRendererNonsecure))); +// Tests the security hardening that prevents a compromised renderer from +// starting a cross-origin SharedWorker from a chrome-extension:// context. +// See https://crbug.com/504073872. +TEST_F(SharedWorkerServiceImplTest, ExtensionCrossOriginSameOriginCheck) { + // Required to make url::Origin recognize "chrome-extension" as a standard + // scheme in the content_unittests environment.
Regression Test / PoC
diff --git a/content/browser/worker_host/shared_worker_service_impl_unittest.cc b/content/browser/worker_host/shared_worker_service_impl_unittest.cc
index fea3d437..3d56bd7 100644
--- a/content/browser/worker_host/shared_worker_service_impl_unittest.cc
+++ b/content/browser/worker_host/shared_worker_service_impl_unittest.cc
@@ -29,6 +29,7 @@
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/mock_render_process_host.h"
+#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_utils.h"
#include "content/test/fake_network_url_loader_factory.h"
@@ -45,6 +46,7 @@
#include "third_party/blink/public/common/messaging/message_port_channel.h"
#include "third_party/blink/public/mojom/worker/shared_worker_info.mojom.h"
#include "url/origin.h"
+#include "url/url_util.h"
using blink::MessagePortChannel;
@@ -1833,4 +1835,196 @@
ContextTypeTestCase::kMismatchRendererSecure,
ContextTypeTestCase::kMismatchRendererNonsecure)));
+// Tests the security hardening that prevents a compromised renderer from
+// starting a cross-origin SharedWorker from a chrome-extension:// context.
+// See https://crbug.com/504073872.
+TEST_F(SharedWorkerServiceImplTest, ExtensionCrossOriginSameOriginCheck) {
+ // Required to make url::Origin recognize "chrome-extension" as a standard
+ // scheme in the content_unittests environment.
+ url::ScopedSchemeRegistryForTests scoped_registry;
+ url::AddStandardScheme("chrome-extension", url::SCHEME_WITH_HOST);
+
+ // Enable the security check feature.
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ features::kEnforceSharedWorkerSameOriginCheck);
+
+ // Set up a renderer process with an extension origin.
+ const GURL kExtensionUrl("chrome-extension://abc/");
+ const GURL kWorkerUrl("https://example.com/worker.js");
+ const char kName[] = "name";
+
+ std::unique_ptr<TestWebContents> web_contents =
+ CreateWebContents(kExtensionUrl);
+ TestRenderFrameHost* render_frame_host = web_contents->GetPrimaryMainFrame();
+ MockRenderProcessHost* renderer_host = render_frame_host->GetProcess();
+
+ EXPECT_EQ(kExtensionUrl, web_contents->GetLastCommittedURL());
+ EXPECT_EQ("chrome-extension",
+ render_frame_host->GetStorageKey().origin().scheme());
+
+ int initial_bad_msg_count = renderer_host->bad_msg_count();
+
+ // Create worker info with a cross-origin script URL (https://example.com).
+ auto options = blink::mojom::WorkerOptions::New();
+ options->name = kName;
+ blink::mojom::SharedWorkerInfoPtr info(blink::mojom::SharedWorkerInfo::New(
+ kWorkerUrl, std::move(options),
+ std::vector<network::mojom::ContentSecurityPolicyPtr>(),
+ blink::mojom::FetchClientSettingsObject::New(
+ []() {
+ auto policies = blink::mojom::PolicyContainerPolicies::New();
+ policies->referrer_policy =
+ network::mojom::ReferrerPolicy::kDefault;
+ return policies;
+ }(),
+ GURL(), blink::mojom::InsecureRequestsPolicy::kDoNotUpgrade),
+ blink::mojom::SharedWorkerSameSiteCookies::kAll,
+ /*extended_lifetime=*/false));
+
+ blink::MessagePortDescriptorPair pipe;
+ mojo::PendingRemote<blink::mojom::SharedWorkerClient> client_proxy;
+ MockSharedWorkerClient client;
+ client.Bind(client_proxy.InitWithNewPipeAndPassReceiver());
+
+ SharedWorkerServiceImpl* service = static_cast<SharedWorkerServiceImpl*>(
+ browser_context_->GetDefaultStoragePartition()->GetSharedWorkerService());
+
+ // Simulate a renderer calling ConnectToWorker with a cross-origin URL.
+ service->ConnectToWorker(
+ render_frame_host->GetGlobalId(), std::move(info),
+ std::move(client_proxy),
+ blink::mojom::SharedWorkerCreationContextType::kSecure,
+ blink::MessagePortChannel(pipe.TakePort1()), nullptr, std::nullopt);
+
+ // The browser process should detect the cross-origin request from the
+ // extension and terminate the renderer (reflected as a bad message count).
+ EXPECT_TRUE(base::test::RunUntil([&]() {
+ return renderer_host->bad_msg_count() > initial_bad_msg_count;
+ }));
+}
+
+// Verifies that the same same-origin hardening also applies to Isolated Web
+// Apps (isolated-app://), preventing a compromised IWA renderer from
+// starting cross-origin SharedWorkers.
+TEST_F(SharedWorkerServiceImplTest, IwaCrossOriginSameOriginCheck) {
+ url::ScopedSchemeRegistryForTests scoped_registry;
+ url::AddStandardScheme("isolated-app", url::SCHEME_WITH_HOST);
+
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ features::kEnforceSharedWorkerSameOriginCheck);
+
+ const GURL kIwaUrl("isolated-app://abc/");
+ const GURL kWorkerUrl("https://example.com/worker.js");
+ const char kName[] = "name";
+
+ std::unique_ptr<TestWebContents> web_contents = CreateWebContents(kIwaUrl);
+ TestRenderFrameHost* render_frame_host = web_contents->GetPrimaryMainFrame();
+ MockRenderProcessHost* renderer_host = render_frame_host->GetProcess();
+
+ int initial_bad_msg_count = renderer_host->bad_msg_count();
+
+ auto options = blink::mojom::WorkerOptions::New();
+ options->name = kName;
+ blink::mojom::SharedWorkerInfoPtr info(blink::mojom::SharedWorkerInfo::New(
+ kWorkerUrl, std::move(options),
+ std::vector<network::mojom::ContentSecurityPolicyPtr>(),
+ blink::mojom::FetchClientSettingsObject::New(
+ []() {
+ auto policies = blink::mojom::PolicyContainerPolicies::New();
+ policies->referrer_policy =
+ network::mojom::ReferrerPolicy::kDefault;
+ return policies;
+ }(),
+ GURL(), blink::mojom::InsecureRequestsPolicy::kDoNotUpgrade),
+ blink::mojom::SharedWorkerSameSiteCookies::kAll,
+ /*extended_lifetime=*/false));
+
+ blink::MessagePortDescriptorPair pipe;
+ mojo::PendingRemote<blink::mojom::SharedWorkerClient> client_proxy;
+ MockSharedWorkerClient client;
+ client.Bind(client_proxy.InitWithNewPipeAndPassReceiver());
+
+ SharedWorkerServiceImpl* service = static_cast<SharedWorkerServiceImpl*>(
+ browser_context_->GetDefaultStoragePartition()->GetSharedWorkerService());
+
+ service->ConnectToWorker(
+ render_frame_host->GetGlobalId(), std::move(info),
+ std::move(client_proxy),
+ blink::mojom::SharedWorkerCreationContextType::kSecure,
+ blink::MessagePortChannel(pipe.TakePort1()), nullptr, std::nullopt);
+
+ // IWA cross-origin request should also be blocked.
+ EXPECT_TRUE(base::test::RunUntil([&]() {
+ return renderer_host->bad_msg_count() > initial_bad_msg_count;
+ }));
+}
+
+// Verifies that when the security feature flag is disabled, cross-origin
+// SharedWorker creation from extensions is allowed (legacy behavior).
+TEST_F(SharedWorkerServiceImplTest, ExtensionSameOriginCheckFlagOff) {
+ url::ScopedSchemeRegistryForTests scoped_registry;
+ url::AddStandardScheme("chrome-extension", url::SCHEME_WITH_HOST);
+
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(
+ features::kEnforceSharedWorkerSameOriginCheck);
+
+ const GURL kExtensionUrl("chrome-extension://abc/");
+ const GURL kWorkerUrl("https://example.com/worker.js");
+ const char kName[] = "name";
+
+ std::unique_ptr<TestWebContents> web_contents =
+ CreateWebContents(kExtensionUrl);
+ TestRenderFrameHost* render_frame_host = web_contents->GetPrimaryMainFrame();
+ MockRenderProcessHost* renderer_host = render_frame_host->GetProcess();
+
+ int initial_bad_msg_count = renderer_host->bad_msg_count();
+
+ auto options = blink::mojom::WorkerOptions::New();
+ options->name = kName;
+ blink::mojom::SharedWorkerInfoPtr info(blink::mojom::SharedWorkerInfo::New(
+ kWorkerUrl, std::move(options),
+ std::vector<network::mojom::ContentSecurityPolicyPtr>(),
+ blink::mojom::FetchClientSettingsObject::New(
+ []() {
+ auto policies = blink::mojom::PolicyContainerPolicies::New();
+ policies->referrer_policy =
+ network::mojom::ReferrerPolicy::kDefault;
+ return policies;
+ }(),
+ GURL(), blink::mojom::InsecureRequestsPolicy::kDoNotUpgrade),
+ blink::mojom::SharedWorkerSameSiteCookies::kAll,
+ /*extended_lifetime=*/false));
+
+ blink::MessagePortDescriptorPair pipe;
+ mojo::PendingRemote<blink::mojom::SharedWorkerClient> client_proxy;
+ MockSharedWorkerClient client;
+ client.Bind(client_proxy.InitWithNewPipeAndPassReceiver());
+
+ SharedWorkerServiceImpl* service = static_cast<SharedWorkerServiceImpl*>(
+ browser_context_->GetDefaultStoragePartition()->GetSharedWorkerService());
+
+ TestSharedWorkerServiceObserver observer;
+ base::ScopedObservation<SharedWorkerService, SharedWorkerService::Observer>
+ observation(&observer);
+ observation.Observe(service);
+
+ service->ConnectToWorker(
+ render_frame_host->GetGlobalId(), std::move(info),
+ std::move(client_proxy),
+ blink::mojom::SharedWorkerCreationContextType::kSecure,
+ blink::MessagePortChannel(pipe.TakePort1()), nullptr, std::nullopt);
+
+ // With the flag off, the connection should proceed to host creation
+ // instead of being blocked with a bad message.
+ EXPECT_TRUE(base::test::RunUntil([&]() {
+ return observer.GetWorkerCount() > 0 ||
+ client.CheckReceivedOnScriptLoadFailed() ||
+ renderer_host->bad_msg_count() > initial_bad_msg_count;
+ }));
+ EXPECT_EQ(initial_bad_msg_count, renderer_host->bad_msg_count());
+}
+
} // namespace content
Original Bug Report
SharedWorker same-origin check bypass for chrome-extension:// β DedicatedWorker hardening (8bde565) not applied to sibling SharedWorker path
Security Bug
Important: Please do not change the component of this bug manually.
Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md
Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs
Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp
NOTE: Security bugs are normally made public once a fix has been widely deployed.
VULNERABILITY DETAILS
A compromised renderer hosting a chrome-extension:// document can construct a SharedWorker whose main-script URL is cross-origin to the extension. The creator scheme chrome-extension is unconditionally whitelisted in ChromeContentBrowserClient::DoesSchemeAllowCrossOriginSharedWorker, bypassing the same-origin check in SharedWorkerServiceImpl::ConnectToWorker. The resulting bidirectional MessagePort enables asset exfiltration (cookies, history, bookmarks, storage) from the extension context to an attacker origin.
This is the sibling of the DedicatedWorker hardening landed 2026-04-14 (commit 8bde565f45a8baf9b84c129905eb53c9abe108d2, “DedicatedWorker: Enforce same-origin check for IWA and Extensions”). The commit message explicitly scopes the hardening to “worker-related destinations (DedicatedWorker, SharedWorker, ServiceWorker)” but the fix only covers DedicatedWorker. ServiceWorker is safe via pre-existing AllOriginsMatchAndCanAccessServiceWorkers. SharedWorker remains unpatched.
Upstream bug reference: crbug/496253755.
VERSION
Chrome Version:
147.0.7727.101stable β SharedWorker vulnerable; DedicatedWorker also passes (pre-patch)- Trunk
HEAD fdd8e3060ddaa@ 2026-04-19 (contains8bde565) β SharedWorker vulnerable; DedicatedWorker blocked viaDWH_INVALID_SCRIPT_URL_ORIGINrenderer kill
Operating System: Linux (Kali 6.19.11, x86_64). Behavior is OS-independent; Linux tested.
Reproduction rate: 100%
REPRODUCTION CASE
Attached files (directly, per template instructions β no archive):
manifest.jsonβ MV3 test extensionpopup.htmlpopup.jsworker.jsβ served by the attacker HTTP serverserver.pyβ python3 stdlib only, serves/worker.js+POST /collect+GET /collectedF009_shared_worker_cross_origin_extension_bypass.mdβ full finding with root-cause analysis,file:linereferences, and suggested patch
Steps
python3 server.pyβ listens on127.0.0.1:8000chrome://extensions/β Developer mode β Load unpacked β select the directory containingmanifest.json/popup.html/popup.js- Click the extension icon to open the popup
- Popup log shows:
[F009] cross-origin SharedWorker constructed (should have been blocked)[F009] server confirmed receipt: [...]
curl http://127.0.0.1:8000/collectedconfirms the exfiltrated payload reached the attacker origin
Complementary DedicatedWorker control probe
Proves the patch ignores CSP and isolates the SharedWorker inconsistency.
Uncomment the block at popup.js:36-42 (new Worker(attacker)) and reproduce on a trunk/Canary build. The browser process terminates the renderer:
ERROR:render_process_host_impl.cc:6197] Terminating render process for bad Mojo message:
Received bad user message: DWH_INVALID_SCRIPT_URL_ORIGIN
ERROR:bad_message.cc:29] Terminating renderer for bad IPC message, reason 123
Same URL, same manifest, same extension origin as the SharedWorker case β only the worker type differs.
Related test coverage
third_party/blink/web_tests/http/tests/security/cross-origin-shared-worker-allowed.html exercises the permissive path affected by this report. Resolving F009 will require updating this test to reflect the new same-origin enforcement for chrome-extension:// creators (or gating its current assertions behind a non-extension context).
FOR CRASHES
N/A β not a crash. The vulnerability is a missing same-origin check enabling cross-origin data exfiltration. No memory corruption, no stack trace.
CREDIT INFORMATION
Reporter credit: VEZEKA
Finding (full analysis)
SharedWorker same-origin check bypass via DoesSchemeAllowCrossOriginSharedWorker whitelist β asset exfiltration from chrome-extension:// context
Reporter finding ID : F009 Date : 2026-04-19 Status : Confirmed on Chrome Stable
Summary
Chrome enforces a same-origin check on SharedWorker main-script URL only when the creator scheme is not in a hard-coded allowlist. The allowlist currently contains chrome-extension://. A compromised renderer executing within a chrome-extension:// security context can therefore instantiate a SharedWorker whose main script is attacker-controlled (https://attacker.example/worker.js), and use the bidirectional MessagePort to exfiltrate data accessible from the extension context (cookies, history, bookmarks, storage) to an attacker origin.
This is the direct sibling of the DedicatedWorker hardening landed on 2026-04-14 (commit 8bde565f45a8baf9b84c129905eb53c9abe108d2, “DedicatedWorker: Enforce same-origin check for IWA and Extensions”). That commit explicitly scopes the hardening to “worker-related destinations (DedicatedWorker, SharedWorker, ServiceWorker)”, but only the DedicatedWorker path was patched. ServiceWorker is protected by pre-existing same-origin enforcement (AllOriginsMatchAndCanAccessServiceWorkers) β SharedWorker is not.
Affected versions
| Channel | Version | SharedWorker cross-origin | Sibling DedicatedWorker (control) |
|---|---|---|---|
| Stable | 147.0.7727.101 | Vulnerable (constructed + exfil) | Also passes β 8bde565 not yet shipped |
| Dev / Trunk | main @ 2026-04-19 (HEAD fdd8e3060ddaa, contains 8bde565) |
Vulnerable (constructed + exfil) | Blocked via DWH_INVALID_SCRIPT_URL_ORIGIN bad-message kill |
| Beta | to be confirmed | Expected vulnerable | Expected blocked once 8bde565 reaches Beta |
The Dev/Trunk row is the decisive evidence: on a binary that contains the DedicatedWorker hardening (kEnforceDedicatedWorkerSameOriginCheck is FEATURE_ENABLED_BY_DEFAULT), SharedWorker still constructs and exfiltrates cross-origin from a chrome-extension:// creator under the same manifest, same attacker origin, same script URL. The inconsistency is structural, not configuration-dependent.
Component
Blink > Workers > SharedWorker
content/browser/worker_host/shared_worker_service_impl.cc
chrome/browser/chrome_content_browser_client.cc
Threat model
Compromised renderer hosting a chrome-extension:// document. This is the exact threat model the sibling DedicatedWorker fix addresses (commit message: “security hardening to prevent asset exfiltration from these contexts by compromised renderer processes”). A renderer can be compromised by any V8 / Blink RCE (example recent class: CVE-2024-xxxx type-confusion in V8). Extensions ship to hundreds of millions of users with host_permissions: ["<all_urls>"] (password managers, ad blockers, shopping assistants, translators), therefore the post-compromise primitive has broad impact.
CSP is not a mitigation under this threat model: a compromised renderer executes native code outside CSP enforcement. The same observation justified the DedicatedWorker fix.
Root cause
Entry point
SharedWorkerConnector.Connect Mojo call from renderer β SharedWorkerConnectorImpl::Connect (content/browser/worker_host/shared_worker_connector_impl.cc:51) β SharedWorkerServiceImpl::ConnectToWorker.
Defective check
content/browser/worker_host/shared_worker_service_impl.cc:167-175
// Enforce same-origin policy.
// data: URLs are not considered a different origin.
bool is_cross_origin = !info->url.SchemeIs(url::kDataScheme) &&
url::Origin::Create(info->url) != storage_key.origin();
if (is_cross_origin &&
!GetContentClient()->browser()->DoesSchemeAllowCrossOriginSharedWorker(
storage_key.origin().scheme())) {
ScriptLoadFailed(std::move(client), /*error_message=*/"");
return;
}
The same-origin check is bypassed whenever DoesSchemeAllowCrossOriginSharedWorker(creator_scheme) returns true.
Defective allowlist
chrome/browser/chrome_content_browser_client.cc:3288-3298
bool ChromeContentBrowserClient::DoesSchemeAllowCrossOriginSharedWorker(
const std::string& scheme) {
#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
// Extensions are allowed to start cross-origin shared workers.
if (scheme == extensions::kExtensionScheme) {
return true;
}
#endif
return false;
}
chrome-extension:// is unconditionally whitelisted. Any cross-origin SharedWorker main-script URL is accepted when the creator is an extension document.
Why the sibling ServiceWorker and DedicatedWorker paths are safe
- DedicatedWorker β patched on 2026-04-14 at
content/browser/worker_host/dedicated_worker_host_factory_impl.cc:136-150with an explicit scheme rejection (mojo::ReportBadMessage("DWH_INVALID_SCRIPT_URL_ORIGIN")) coveringisolated-appandchrome-extension. - ServiceWorker β registration goes through
AllOriginsMatchAndCanAccessServiceWorkersinservice_worker_container_host.cc:140-150, which requires scope, script URL, and storage key origin to match exactly. No scheme allowlist bypass exists.
SharedWorker is therefore the only remaining worker surface still exposing the pre-hardening behavior.
Related surfaces audited and not vulnerable
isolated-app://β not present inDoesSchemeAllowCrossOriginSharedWorker(onlychrome-extension).- Worklet hosts (
AuctionWorkletManager, AnimationWorklet / PaintWorklet / LayoutWorklet / AudioWorklet) β no equivalent scheme-based bypass. SharedStorageWorkletHostβ partitioned bydata_origin; separate analysis required but outside this report.
Reproduction
PoC is provided as separate files per the 2026-03 VRP formatting rules.
poc/
βββ extension/
β βββ manifest.json
β βββ popup.html
β βββ popup.js
βββ attacker/
βββ server.py # Python HTTP server (serves worker.js and /collect endpoint)
βββ worker.js # Cross-origin SharedWorker script
Steps
cd poc/attacker && python3 server.pyβ serveshttp://127.0.0.1:8000/.- In Chrome Stable 147.0.7727.101:
chrome://extensions/β Developer mode β Load unpacked β selectpoc/extension/. - Note the extension id printed for reference in the report (
chrome-extension://<EXT_ID>/). - Click the extension icon to open the popup.
- Observe the popup log: cross-origin
SharedWorkeris constructed, extension-accessible data is sent viaMessagePort.postMessage, and the server’s/collectedendpoint confirms reception.
Observed output
creator origin = chrome-extension://<EXT_ID>
creator scheme = chrome-extension:
SharedWorker constructor returned without throwing
postMessage sent
worker reply: {"hello":"from attacker worker","url":"http://127.0.0.1:8000/worker.js"}
server collected endpoint: [{"exfil_secret":"...","origin":"chrome-extension://<EXT_ID>","ts":...}]
self.location.href inside the worker resolves to http://127.0.0.1:8000/worker.js, proving the main-script executed cross-origin without a same-origin violation.
Caveat on the PoC manifest
The manifest contains "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'; worker-src http://127.0.0.1:8000" } purely as a test-harness convenience so the PoC is driveable from a benign extension popup. Under the intended threat model (compromised renderer), CSP is bypassed natively and the permissive worker-src is not a precondition for exploitation. This aligns with the rationale of the DedicatedWorker fix, which does not rely on CSP either.
Complementary validation β control vs variant on the same binary
To rule out any CSP / manifest-opt-in interpretation, the PoC was extended with a control probe that attempts new Worker(attacker) immediately before the SharedWorker construction, using the exact same URL, manifest, and extension origin. On a Debug build of trunk HEAD (contains 8bde565, kEnforceDedicatedWorkerSameOriginCheck enabled by default):
[Test A] about to test DedicatedWorker cross-origin to: http://127.0.0.1:8000/worker.js
[Test A] new Worker() returned without JS throw
ERROR:render_process_host_impl.cc:6197] Terminating render process for bad Mojo message:
Received bad user message: DWH_INVALID_SCRIPT_URL_ORIGIN
ERROR:bad_message.cc:29] Terminating renderer for bad IPC message, reason 123
[renderer killed β extension popup crashed]
In a separate run (control probe commented out), new SharedWorker(attacker) from the same extension popup succeeded, the worker’s self.location.href resolved to http://127.0.0.1:8000/worker.js, and the attacker server’s /collected endpoint recorded the exfiltrated payload. Only the worker type differs; DedicatedWorker is killed by the browser process, SharedWorker is not checked at all. This demonstrates the bypass is independent of CSP (the patch enforces the check server-side regardless of worker-src) and confirms the hardening is missing specifically on the SharedWorker path.
Impact
Primitive: from a compromised chrome-extension:// renderer, open a MessagePort to an arbitrary cross-origin script URL and exchange data bidirectionally without tripping any browser-process check.
With typical extension host_permissions:
| Data source | API | Exfiltrable |
|---|---|---|
| Cookies (all domains) | chrome.cookies.getAll({}) |
Yes |
| Browser history | chrome.history.search({text:""}) |
Yes |
| Bookmarks | chrome.bookmarks.getTree() |
Yes |
Classification: cross-origin data leak reachable from a compromised renderer in an extension context β Site Isolation / UXSS-class primitive.
Suggested patch
Align SharedWorkerServiceImpl::ConnectToWorker with the DedicatedWorker hardening landed in 8bde565f45a8b, gated by a new feature flag for staged rollout.
// content/browser/worker_host/shared_worker_service_impl.cc
// After the existing is_cross_origin check at line ~175:
if (base::FeatureList::IsEnabled(
features::kEnforceSharedWorkerSameOriginCheck) &&
!info->url.SchemeIs(url::kDataScheme)) {
constexpr char kIsolatedAppScheme[] = "isolated-app";
constexpr char kExtensionScheme[] = "chrome-extension";
const std::string& creator_scheme = storage_key.origin().scheme();
const std::string& script_scheme = url::Origin::Create(info->url).scheme();
const bool involves_restricted_scheme =
creator_scheme == kIsolatedAppScheme ||
creator_scheme == kExtensionScheme ||
script_scheme == kIsolatedAppScheme ||
script_scheme == kExtensionScheme;
if (involves_restricted_scheme &&
url::Origin::Create(info->url) != storage_key.origin()) {
bad_message::ReceivedBadMessage(
host, bad_message::SWSI_CROSS_ORIGIN_SCRIPT_URL);
return;
}
}
Alternative, more surgical: drop chrome-extension from DoesSchemeAllowCrossOriginSharedWorker. This carries a small risk of breaking Manifest V2 extensions relying on cross-origin SharedWorker; the feature-flag approach is recommended.
A new bad_message enum entry (e.g. SWSI_CROSS_ORIGIN_SCRIPT_URL) should be added to content/browser/bad_message.h, mirroring DWH_INVALID_SCRIPT_URL_ORIGIN.
Bisect
The permissive behavior has existed as long as DoesSchemeAllowCrossOriginSharedWorker has whitelisted chrome-extension (predates the current main branch history window; introduced for Manifest V2 compatibility). The defect became security-relevant on 2026-04-14 when the sibling DedicatedWorker path was hardened, creating an inconsistency in the worker security posture.
- Hardening commit (reference):
8bde565f45a8baf9b84c129905eb53c9abe108d2β “DedicatedWorker: Enforce same-origin check for IWA and Extensions” βrefs/heads/main@{#1614854}. - Vulnerable code path (SharedWorker): present at main
HEADas of 2026-04-19.
References
- Sibling fix:
8bde565f45a8baf9b84c129905eb53c9abe108d2 - DedicatedWorker enforcement:
content/browser/worker_host/dedicated_worker_host_factory_impl.cc:136-150 - ServiceWorker pre-existing protection:
content/browser/service_worker/service_worker_container_host.cc:140-150βAllOriginsMatchAndCanAccessServiceWorkers - Defective allowlist:
chrome/browser/chrome_content_browser_client.cc:3288-3298 - Defective check site:
content/browser/worker_host/shared_worker_service_impl.cc:167-175
Reporter notes
- PoC verified on 2026-04-19 (Linux Kali 6.19.11, x86_64) against:
- Chrome Stable 147.0.7727.101 (Google-signed binary): SharedWorker cross-origin from
chrome-extension://constructed + exfil reached attacker origin. DedicatedWorker also passes (pre-patch baseline β8bde565not yet shipped to Stable). - Chrome Dev /
google-chrome-unstable149.0.7795.2 (Google-signed binary): same SharedWorker behavior observed. - Local Debug build of trunk
HEAD fdd8e3060ddaa(contains8bde565,kEnforceDedicatedWorkerSameOriginCheck = FEATURE_ENABLED_BY_DEFAULT): SharedWorker cross-origin still constructs and exfiltrates; DedicatedWorker cross-origin triggersmojo::ReportBadMessage("DWH_INVALID_SCRIPT_URL_ORIGIN")and terminates the renderer (render_process_host_impl.cc:6197, bad_message reason 123). Captured in the same run, same PID, within ~5 ms β isolating worker type as the only variable.
- Chrome Stable 147.0.7727.101 (Google-signed binary): SharedWorker cross-origin from
- Evidence attached to the tracker issue:
kill_signature.txtβ 4-line extract of the DedicatedWorker kill + SharedWorker construction.trunk_debug_kill_context.logβ 71-line context window around the kill event.attacker_collected.jsonβ full dump of the attacker server’s/collectedendpoint.trunk_debug_both_workers.log/trunk_debug_dedicated_worker_kill.logβ full stderr logs (6.2β6.6 MB each) available on request.- Screenshots of the extension popup log, the
/collectedresponse, and the Chromium renderer-crash notification triggered by the DedicatedWorker bad-message kill.
- No third-party code is involved.
- No embargo requested.