Overview

Medium
Severity
β€”
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Workers
DescriptionInsufficient policy enforcement in Workers
ComponentWorkers
Bug ClassLogic Error
Tracker504073872
Fix commit0e9234d7c0a1 (chromium/src) +261/-12
CISA KEVNot listed
CreditedVEZEKA
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
chrome/browser/chrome_content_browser_client.cc
modified
TEST_F
content/browser/worker_host/shared_worker_service_impl_unittest.cc
modified

Files Changed

  • chrome/browser/chrome_content_browser_client.cc
  • chrome/browser/chrome_content_browser_client.h
  • content/browser/bad_message.h
  • content/browser/worker_host/dedicated_worker_host.cc
  • content/browser/worker_host/shared_worker_service_impl.cc
  • content/browser/worker_host/shared_worker_service_impl_unittest.cc
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.
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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.101 stable β€” SharedWorker vulnerable; DedicatedWorker also passes (pre-patch)
  • Trunk HEAD fdd8e3060ddaa @ 2026-04-19 (contains 8bde565) β€” SharedWorker vulnerable; DedicatedWorker blocked via DWH_INVALID_SCRIPT_URL_ORIGIN renderer 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 extension
  • popup.html
  • popup.js
  • worker.js β€” served by the attacker HTTP server
  • server.py β€” python3 stdlib only, serves /worker.js + POST /collect + GET /collected
  • F009_shared_worker_cross_origin_extension_bypass.md β€” full finding with root-cause analysis, file:line references, and suggested patch

Steps

  1. python3 server.py β€” listens on 127.0.0.1:8000
  2. chrome://extensions/ β†’ Developer mode β†’ Load unpacked β†’ select the directory containing manifest.json / popup.html / popup.js
  3. Click the extension icon to open the popup
  4. Popup log shows:
    • [F009] cross-origin SharedWorker constructed (should have been blocked)
    • [F009] server confirmed receipt: [...]
  5. curl http://127.0.0.1:8000/collected confirms 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.

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-150 with an explicit scheme rejection (mojo::ReportBadMessage("DWH_INVALID_SCRIPT_URL_ORIGIN")) covering isolated-app and chrome-extension.
  • ServiceWorker β€” registration goes through AllOriginsMatchAndCanAccessServiceWorkers in service_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.

  • isolated-app:// β€” not present in DoesSchemeAllowCrossOriginSharedWorker (only chrome-extension).
  • Worklet hosts (AuctionWorkletManager, AnimationWorklet / PaintWorklet / LayoutWorklet / AudioWorklet) β€” no equivalent scheme-based bypass.
  • SharedStorageWorkletHost β€” partitioned by data_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

  1. cd poc/attacker && python3 server.py β€” serves http://127.0.0.1:8000/.
  2. In Chrome Stable 147.0.7727.101: chrome://extensions/ β†’ Developer mode β†’ Load unpacked β†’ select poc/extension/.
  3. Note the extension id printed for reference in the report (chrome-extension://<EXT_ID>/).
  4. Click the extension icon to open the popup.
  5. Observe the popup log: cross-origin SharedWorker is constructed, extension-accessible data is sent via MessagePort.postMessage, and the server’s /collected endpoint 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 HEAD as 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 β€” 8bde565 not yet shipped to Stable).
    • Chrome Dev / google-chrome-unstable 149.0.7795.2 (Google-signed binary): same SharedWorker behavior observed.
    • Local Debug build of trunk HEAD fdd8e3060ddaa (contains 8bde565, kEnforceDedicatedWorkerSameOriginCheck = FEATURE_ENABLED_BY_DEFAULT): SharedWorker cross-origin still constructs and exfiltrates; DedicatedWorker cross-origin triggers mojo::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.
  • 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 /collected endpoint.
    • 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 /collected response, and the Chromium renderer-crash notification triggered by the DedicatedWorker bad-message kill.
  • No third-party code is involved.
  • No embargo requested.
View on issue tracker