Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Internals
DescriptionUse after free in Internals
ComponentInternals
Sub-componentInternals
Bug ClassUAF
Tracker557501968
Fix commit561b7f17d702 (chromium/src) +157/-8
CISA KEVNot listed
Creditedweihengqiuu
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
TEST_F
content/browser/worker_host/shared_worker_service_impl_unittest.cc
modified
PdfWorkerTest
content/browser/worker_host/worker_browsertest.cc
modified
PdfWorkerTest
content/browser/worker_host/worker_browsertest.cc
modified
IN_PROC_BROWSER_TEST_F
content/browser/worker_host/worker_browsertest.cc
modified

Files Changed

  • content/browser/worker_host/worker_browsertest.cc
  • content/browser/worker_host/shared_worker_service_impl_unittest.cc
  • content/browser/worker_host/shared_worker_service_impl.cc
From 561b7f17d702bf393401ec38c72f7c19ad94ff3e Mon Sep 17 00:00:00 2001
From: Yoshisto Yanagisawa <yyanagisawa@chromium.org>
Date: Thu, 17 Sep 2026 18:09:53 -0700
Subject: [PATCH] Check process data access permission before connecting shared workers

Verify that the requesting frame's process has origin data access
permission in SharedWorkerServiceImpl::ConnectToWorker before matching
or creating a shared worker host.

This aligns shared worker creation with existing storage and worker
connection requirements for processes that do not have data access
enabled for the storage key's origin (such as PDF renderer processes).

TAG=agy
CONV=2da8886f-e748-4ec7-b9b5-ac1e517e8525

Bug: 557501968
Change-Id: Ied14bffd701485e8763ccc850a461289edea887f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8376281
Commit-Queue: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Reviewed-by: Hiroki Nakagawa <nhiroki@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1700357}
---
diff --git a/content/browser/worker_host/shared_worker_service_impl.cc b/content/browser/worker_host/shared_worker_service_impl.cc
index 7b3ffff..41477b66 100644
--- a/content/browser/worker_host/shared_worker_service_impl.cc
+++ b/content/browser/worker_host/shared_worker_service_impl.cc
@@ -184,6 +184,15 @@
   const blink::StorageKey& storage_key =
       storage_key_override.value_or(render_frame_host->GetStorageKey());
 
+  // Ensure that the requesting process can access data for the storage key's
+  // origin before connecting to or creating a shared worker.
+  if (!ChildProcessSecurityPolicyImpl::GetInstance()->CanAccessDataForOrigin(
+          render_frame_host->GetProcess()->GetID().value(),
+          storage_key.origin())) {
+    ScriptLoadFailed(std::move(client), /*error_message=*/"");
+    return;
+  }
+
   if (base::FeatureList::IsEnabled(
           features::kEnforceSharedWorkerSameOriginCheck) &&
       !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 722640ee..e319589 100644
--- a/content/browser/worker_host/shared_worker_service_impl_unittest.cc
+++ b/content/browser/worker_host/shared_worker_service_impl_unittest.cc
@@ -21,8 +21,12 @@
 #include "base/test/run_until.h"
 #include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
+#include "content/browser/embedder_isolation_info.h"
+#include "content/browser/process_lock.h"
 #include "content/browser/renderer_host/render_process_host_impl.h"
+#include "content/browser/security/cpsp/child_process_security_policy_impl.h"
 #include "content/browser/site_instance_impl.h"
+#include "content/browser/url_info.h"
 #include "content/browser/worker_host/mock_shared_worker.h"
 #include "content/browser/worker_host/shared_worker_connector_impl.h"
 #include "content/common/features.h"
@@ -2099,4 +2103,67 @@
   EXPECT_FALSE(rfh_impl3->is_evicted_from_back_forward_cache());
 }
 
+// Verifies that ConnectToWorker fails with OnScriptLoadFailed when the
+// requesting process does not have data access permission for the origin.
+TEST_F(SharedWorkerServiceImplTest, CanAccessDataForOriginDenied) {
+  const GURL kUrl("https://example.com/");
+  const GURL kWorkerUrl("https://example.com/worker.js");
+
+  std::unique_ptr<TestWebContents> web_contents = CreateWebContents(kUrl);
+  TestRenderFrameHost* render_frame_host = web_contents->GetPrimaryMainFrame();
+  MockRenderProcessHost* renderer_host = render_frame_host->GetProcess();
+
+  UrlInfo pdf_url_info(UrlInfoInit(kUrl).WithEmbedderIsolationInfo(
+      EmbedderIsolationInfo::CreateForPdf()));
+  scoped_refptr<SiteInstanceImpl> pdf_site_instance =
+      SiteInstanceImpl::CreateForUrlInfo(browser_context_.get(), pdf_url_info,
+                                         /*is_guest=*/false,
+                                         /*is_fenced=*/false,
+                                         /*is_fixed_storage_partition=*/false);
+  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
+  policy->Remove(renderer_host->GetID());
+  policy->AddForTesting(renderer_host->GetID(), browser_context_.get());
+  policy->LockProcess(
+      pdf_site_instance->GetIsolationContext(), renderer_host->GetID(),
+      /*is_process_used=*/false,
+      ProcessLock::FromSiteInfo(pdf_site_instance->GetSiteInfo()));
+
+  ASSERT_FALSE(policy->CanAccessDataForOrigin(
+      renderer_host->GetID().value(),
+      render_frame_host->GetStorageKey().origin()));
+
+  auto options = blink::mojom::WorkerOptions::New();
+  options->name = "name";
+  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);
+
+  EXPECT_TRUE(base::test::RunUntil(
+      [&]() { return client.CheckReceivedOnScriptLoadFailed(); }));
+}
+
 }  // namespace content
diff --git a/content/browser/worker_host/worker_browsertest.cc b/content/browser/worker_host/worker_browsertest.cc
index 27e0a1e..939ecd86 100644
--- a/content/browser/worker_host/worker_browsertest.cc
+++ b/content/browser/worker_host/worker_browsertest.cc
@@ -72,6 +72,7 @@
 #include "services/network/public/mojom/url_response_head.mojom.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/common/storage_key/storage_key.h"
+#include "ui/base/page_transition_types.h"
 #include "url/gurl.h"
 
 namespace content {
@@ -242,6 +243,18 @@
     runner->Run();
   }
 
+  bool NavigateToURLWithPdf(WebContents* web_contents, const GURL& url) {
+    NavigationController::LoadURLParams params(url);
+    params.transition_type = ui::PageTransitionFromInt(
+        ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
+    params.is_pdf = true;
+    NavigateToURLBlockUntilNavigationsComplete(
+        web_contents, params, 1,
+        /*ignore_uncommitted_navigations=*/false);
+    return IsLastCommittedEntryOfPageType(web_contents, PAGE_TYPE_NORMAL) &&
+           web_contents->GetLastCommittedURL() == url;
+  }
+
   void SetSameSiteCookie(const std::string& host) {
     StoragePartition* partition = shell()
                                       ->web_contents()
@@ -525,14 +538,7 @@
 
   // Commit `url` as PDF content so that the resulting frame runs in a process
   // whose SiteInfo has `is_pdf` set.
-  NavigationController::LoadURLParams params(url);
-  params.transition_type = ui::PageTransitionFromInt(
-      ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
-  params.is_pdf = true;
-  NavigateToURLBlockUntilNavigationsComplete(
-      tab, params, 1, /*ignore_uncommitted_navigations=*/false);
-  ASSERT_TRUE(IsLastCommittedEntryOfPageType(tab, PAGE_TYPE_NORMAL));
-  ASSERT_EQ(url, tab->GetLastCommittedURL());
+  ASSERT_TRUE(NavigateToURLWithPdf(tab, url));
 
   RenderFrameHostImpl* frame = tab->GetPrimaryMainFrame();
   ASSERT_TRUE(frame->GetSiteInstance()->GetSiteInfo().is_pdf());
@@ -599,6 +605,73 @@
   EXPECT_TRUE(GetSharedWorkerHost(worker_url));
 }
 
+class PdfWorkerTest : public WorkerTest {
+ public:
+  PdfWorkerTest() {
+    // In content_shell (content_browsertests), ContentRendererClient does not
+    // override IsDomStorageDisabled() for PDF processes (unlike Chrome). As a
+    // result, calling `new SharedWorker(...)` in JS invokes
+    // LocalDOMWindow::GetPublicURLManager(), which attempts to bind
+    // BlobURLStore and triggers a renderer kill under
+    // kEnforcePdfBlobRestrictions before SharedWorkerConnector::Connect reaches
+    // the browser process. Disable kEnforcePdfBlobRestrictions so that the
+    // SharedWorker connection request reaches SharedWorkerServiceImpl.
+    feature_list_.InitAndDisableFeature(
+        blink::features::kEnforcePdfBlobRestrictions);
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+// Verify that frames in a PDF process do not connect to or create shared
+// workers, while an ordinary WebContents at the same URL connects normally.
+IN_PROC_BROWSER_TEST_F(PdfWorkerTest, PdfCannotUseSharedWorker) {
+  if (!SupportsSharedWorker()) {
+    return;
+  }
+
+  const GURL page_url = ssl_server()->GetURL("a.test", "/title1.html");
+  const GURL worker_url =
+      ssl_server()->GetURL("a.test", "/workers/messageport_worker.js");
+  static constexpr char kConnectSharedWorker[] = R"(
+    new Promise(resolve => {
+      window.worker = new SharedWorker("/workers/messageport_worker.js");
+      window.worker.onerror = (e) => resolve("Worker blocked.");
+      window.worker.port.onmessage = (e) => resolve(e.data);
+    })
+  )";
+
+  // 1. A PDF frame attempting to create a shared worker is blocked.
+  ASSERT_TRUE(NavigateToURLWithPdf(shell()->web_contents(), page_url));
+  EXPECT_TRUE(
+      shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()->IsPdf());
+  EXPECT_EQ("Worker blocked.",
+            EvalJs(shell()->web_contents(), kConnectSharedWorker));
+  EXPECT_FALSE(GetSharedWorkerHost(worker_url));
+
+  // 2. Control: an ordinary WebContents at the same URL connects and creates
+  // the shared worker normally.
+  Shell* ordinary_shell = CreateBrowser();
+  ASSERT_TRUE(NavigateToURL(ordinary_shell, page_url));
+  RenderFrameHostImpl* ordinary_rfh = static_cast<RenderFrameHostImpl*>(
+      ordinary_shell->web_contents()->GetPrimaryMainFrame());
+  EXPECT_FALSE(ordinary_rfh->GetProcess()->IsPdf());
+  EXPECT_EQ("Worker connected.", EvalJs(ordinary_shell, kConnectSharedWorker));
+  SharedWorkerHost* host = GetSharedWorkerHost(worker_url);
+  ASSERT_TRUE(host);
+  EXPECT_EQ(std::vector<GlobalRenderFrameHostId>{ordinary_rfh->GetGlobalId()},
+            host->GetRenderFrameIDsForWorker());
+
+  // 3. A PDF frame is also blocked from connecting to the existing shared
+  // worker, and the existing worker host remains unaffected.
+  EXPECT_EQ("Worker blocked.",
+            EvalJs(shell()->web_contents(), kConnectSharedWorker));
+  EXPECT_EQ(host, GetSharedWorkerHost(worker_url));
+  EXPECT_EQ(std::vector<GlobalRenderFrameHostId>{ordinary_rfh->GetGlobalId()},
+            host->GetRenderFrameIDsForWorker());
+}
+
 // Create a SharedWorker from a COEP:required-corp document.
 IN_PROC_BROWSER_TEST_F(WorkerTest, SharedWorkerInCOEPRequireCorpDocument) {
   if (!SupportsSharedWorker())
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 722640ee..e319589 100644
--- a/content/browser/worker_host/shared_worker_service_impl_unittest.cc
+++ b/content/browser/worker_host/shared_worker_service_impl_unittest.cc
@@ -21,8 +21,12 @@
 #include "base/test/run_until.h"
 #include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
+#include "content/browser/embedder_isolation_info.h"
+#include "content/browser/process_lock.h"
 #include "content/browser/renderer_host/render_process_host_impl.h"
+#include "content/browser/security/cpsp/child_process_security_policy_impl.h"
 #include "content/browser/site_instance_impl.h"
+#include "content/browser/url_info.h"
 #include "content/browser/worker_host/mock_shared_worker.h"
 #include "content/browser/worker_host/shared_worker_connector_impl.h"
 #include "content/common/features.h"
@@ -2099,4 +2103,67 @@
   EXPECT_FALSE(rfh_impl3->is_evicted_from_back_forward_cache());
 }
 
+// Verifies that ConnectToWorker fails with OnScriptLoadFailed when the
+// requesting process does not have data access permission for the origin.
+TEST_F(SharedWorkerServiceImplTest, CanAccessDataForOriginDenied) {
+  const GURL kUrl("https://example.com/");
+  const GURL kWorkerUrl("https://example.com/worker.js");
+
+  std::unique_ptr<TestWebContents> web_contents = CreateWebContents(kUrl);
+  TestRenderFrameHost* render_frame_host = web_contents->GetPrimaryMainFrame();
+  MockRenderProcessHost* renderer_host = render_frame_host->GetProcess();
+
+  UrlInfo pdf_url_info(UrlInfoInit(kUrl).WithEmbedderIsolationInfo(
+      EmbedderIsolationInfo::CreateForPdf()));
+  scoped_refptr<SiteInstanceImpl> pdf_site_instance =
+      SiteInstanceImpl::CreateForUrlInfo(browser_context_.get(), pdf_url_info,
+                                         /*is_guest=*/false,
+                                         /*is_fenced=*/false,
+                                         /*is_fixed_storage_partition=*/false);
+  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
+  policy->Remove(renderer_host->GetID());
+  policy->AddForTesting(renderer_host->GetID(), browser_context_.get());
+  policy->LockProcess(
+      pdf_site_instance->GetIsolationContext(), renderer_host->GetID(),
+      /*is_process_used=*/false,
+      ProcessLock::FromSiteInfo(pdf_site_instance->GetSiteInfo()));
+
+  ASSERT_FALSE(policy->CanAccessDataForOrigin(
+      renderer_host->GetID().value(),
+      render_frame_host->GetStorageKey().origin()));
+
+  auto options = blink::mojom::WorkerOptions::New();
+  options->name = "name";
+  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);
+
+  EXPECT_TRUE(base::test::RunUntil(
+      [&]() { return client.CheckReceivedOnScriptLoadFailed(); }));
+}
+
 }  // namespace content
diff --git a/content/browser/worker_host/worker_browsertest.cc b/content/browser/worker_host/worker_browsertest.cc
index 27e0a1e..939ecd86 100644
--- a/content/browser/worker_host/worker_browsertest.cc
+++ b/content/browser/worker_host/worker_browsertest.cc
@@ -72,6 +72,7 @@
 #include "services/network/public/mojom/url_response_head.mojom.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/common/storage_key/storage_key.h"
+#include "ui/base/page_transition_types.h"
 #include "url/gurl.h"
 
 namespace content {
@@ -242,6 +243,18 @@
     runner->Run();
   }
 
+  bool NavigateToURLWithPdf(WebContents* web_contents, const GURL& url) {
+    NavigationController::LoadURLParams params(url);
+    params.transition_type = ui::PageTransitionFromInt(
+        ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
+    params.is_pdf = true;
+    NavigateToURLBlockUntilNavigationsComplete(
+        web_contents, params, 1,
+        /*ignore_uncommitted_navigations=*/false);
+    return IsLastCommittedEntryOfPageType(web_contents, PAGE_TYPE_NORMAL) &&
+           web_contents->GetLastCommittedURL() == url;
+  }
+
   void SetSameSiteCookie(const std::string& host) {
     StoragePartition* partition = shell()
                                       ->web_contents()
@@ -525,14 +538,7 @@
 
   // Commit `url` as PDF content so that the resulting frame runs in a process
   // whose SiteInfo has `is_pdf` set.
-  NavigationController::LoadURLParams params(url);
-  params.transition_type = ui::PageTransitionFromInt(
-      ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
-  params.is_pdf = true;
-  NavigateToURLBlockUntilNavigationsComplete(
-      tab, params, 1, /*ignore_uncommitted_navigations=*/false);
-  ASSERT_TRUE(IsLastCommittedEntryOfPageType(tab, PAGE_TYPE_NORMAL));
-  ASSERT_EQ(url, tab->GetLastCommittedURL());
+  ASSERT_TRUE(NavigateToURLWithPdf(tab, url));
 
   RenderFrameHostImpl* frame = tab->GetPrimaryMainFrame();
   ASSERT_TRUE(frame->GetSiteInstance()->GetSiteInfo().is_pdf());
@@ -599,6 +605,73 @@
   EXPECT_TRUE(GetSharedWorkerHost(worker_url));
 }
 
+class PdfWorkerTest : public WorkerTest {
+ public:
+  PdfWorkerTest() {
+    // In content_shell (content_browsertests), ContentRendererClient does not
+    // override IsDomStorageDisabled() for PDF processes (unlike Chrome). As a
+    // result, calling `new SharedWorker(...)` in JS invokes
+    // LocalDOMWindow::GetPublicURLManager(), which attempts to bind
+    // BlobURLStore and triggers a renderer kill under
+    // kEnforcePdfBlobRestrictions before SharedWorkerConnector::Connect reaches
+    // the browser process. Disable kEnforcePdfBlobRestrictions so that the
+    // SharedWorker connection request reaches SharedWorkerServiceImpl.
+    feature_list_.InitAndDisableFeature(
+        blink::features::kEnforcePdfBlobRestrictions);
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+// Verify that frames in a PDF process do not connect to or create shared
+// workers, while an ordinary WebContents at the same URL connects normally.
+IN_PROC_BROWSER_TEST_F(PdfWorkerTest, PdfCannotUseSharedWorker) {
+  if (!SupportsSharedWorker()) {
+    return;
+  }
+
+  const GURL page_url = ssl_server()->GetURL("a.test", "/title1.html");
+  const GURL worker_url =
+      ssl_server()->GetURL("a.test", "/workers/messageport_worker.js");
+  static constexpr char kConnectSharedWorker[] = R"(
+    new Promise(resolve => {
+      window.worker = new SharedWorker("/workers/messageport_worker.js");
+      window.worker.onerror = (e) => resolve("Worker blocked.");
+      window.worker.port.onmessage = (e) => resolve(e.data);
+    })
+  )";
+
+  // 1. A PDF frame attempting to create a shared worker is blocked.
+  ASSERT_TRUE(NavigateToURLWithPdf(shell()->web_contents(), page_url));
+  EXPECT_TRUE(
+      shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()->IsPdf());
+  EXPECT_EQ("Worker blocked.",
+            EvalJs(shell()->web_contents(), kConnectSharedWorker));
+  EXPECT_FALSE(GetSharedWorkerHost(worker_url));
+
+  // 2. Control: an ordinary WebContents at the same URL connects and creates
+  // the shared worker normally.
+  Shell* ordinary_shell = CreateBrowser();
+  ASSERT_TRUE(NavigateToURL(ordinary_shell, page_url));
+  RenderFrameHostImpl* ordinary_rfh = static_cast<RenderFrameHostImpl*>(
+      ordinary_shell->web_contents()->GetPrimaryMainFrame());
+  EXPECT_FALSE(ordinary_rfh->GetProcess()->IsPdf());
+  EXPECT_EQ("Worker connected.", EvalJs(ordinary_shell, kConnectSharedWorker));
+  SharedWorkerHost* host = GetSharedWorkerHost(worker_url);
+  ASSERT_TRUE(host);
+  EXPECT_EQ(std::vector<GlobalRenderFrameHostId>{ordinary_rfh->GetGlobalId()},
+            host->GetRenderFrameIDsForWorker());
+
+  // 3. A PDF frame is also blocked from connecting to the existing shared
+  // worker, and the existing worker host remains unaffected.
+  EXPECT_EQ("Worker blocked.",
+            EvalJs(shell()->web_contents(), kConnectSharedWorker));
+  EXPECT_EQ(host, GetSharedWorkerHost(worker_url));
+  EXPECT_EQ(std::vector<GlobalRenderFrameHostId>{ordinary_rfh->GetGlobalId()},
+            host->GetRenderFrameIDsForWorker());
+}
+
 // Create a SharedWorker from a COEP:required-corp document.
 IN_PROC_BROWSER_TEST_F(WorkerTest, SharedWorkerInCOEPRequireCorpDocument) {
   if (!SupportsSharedWorker())
Loading diff…

Original Bug Report

reported by we...@gmail.com

A PDF content frame reads OPFS data through a same-origin SharedWorker that its own process is denied


Report description

A PDF content frame reads OPFS data through a same-origin SharedWorker that its own process is denied


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules


The problem

Please describe the technical details of the vulnerability

A PDF renderer is not allowed to reach the origin’s persistent storage, even though the PDF document carries that origin. The restriction is enforced per process at each storage API’s entry point. A same-origin SharedWorker runs in an ordinary renderer process, passes that check, and is reachable from any same-origin document over a MessagePort — so a PDF content frame can ask it for the data and get it, while the identical call from that frame is refused.

Source refs: chromium/src main @ 3a7b808c7c30e67422b004e4922536bb865195f5 (2026-09-04).

SharedWorkerServiceImpl::ConnectToWorker (shared_worker_service_impl.cc:126) has no CanAccessDataForOrigin() check, before FindMatchingSharedWorkerHost or otherwise. Its only check of this kind is at :148-157:

    const std::optional<WebContents::PrivilegedParams>& privileged_params =
        web_contents->privileged_params();
    if (privileged_params && privileged_params->disallow_shared_workers) {
      ScriptLoadFailed(std::move(client), /*error_message=*/"");
      return;
    }

A PDF tab does not set PrivilegedParams; PDF is carried as EmbedderIsolationInfo::Mode::kPdf. browser_interface_binders.cc:453 binds SharedWorkerConnector for any frame; worker_util.cc CalculateWorkerStorageKey gives the worker the frame’s key; browser_interface_binders.cc:1671 gives the worker the storage binders.

The check that refuses the frame is applied to whichever process calls — file_system_access_manager_impl.cc:613:

  if (!ChildProcessSecurityPolicy::GetInstance()->CanAccessDataForOrigin(
          binding_context.process_id(), binding_context.storage_key.origin())) {

Bisection

  • CL 7894258 — issue 517484284, High CVE-2026-12460 — added the CanAccessDataForOrigin() check above so a compromised PDF renderer cannot reach persistent storage directly.
  • CL 8178927 added the disallow_shared_workers check above, in this same function, for WebContents::PrivilegedParams. Its message states that matching a SharedWorker bridges a privileged renderer to ordinary same-origin content and defeats process isolation.
  • CL 8164422 (in progress) adds PDF checks to the direct CacheStorage/IndexedDB/Bucket binds. It does not touch ConnectToWorker.

Impact analysis

Reproduction

154.0.8034.0 (Chrome for Testing), Windows 11 x64. No flags beyond a disposable --user-data-dir and --remote-debugging-port. DevTools only executes script inside the PDF frame; the premise is a compromised PDF renderer, which is what the CL 7894258 check exists for.

Files: srv.py, cycle.ps1, cdp.ps1, probe.pdf, in one directory. srv.py serves origin A on http://localhost:8841; http://127.0.0.1:8841 is the same server under a second hostname, used only as cross-site origin B.

  • /seed.html (A) writes a random per-run sentinel to OPFS secret.txt, then starts new SharedWorker('/sw.js', {name:'probe'}). /sw.js reads that file on request and posts it back through its port.
  • /mid.html (B) embeds /seed.html. /embed.html (A, top level) embeds /mid.html and /probe.pdf.

The A -> B -> A nesting is required: the PDF content OOPIF’s ancestry is A -> chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai -> pdf, so its key is (A, top-level site A, AncestorChainBit::kCrossSite). Seeding from a top-level A page writes a first-party key, the partitions differ, and the worker returns WORKER-ERR:SecurityError.

Run powershell -ExecutionPolicy Bypass -File cycle.ps1. It launches Chrome on /embed.html, finds the PDF content frame among the DevTools targets, and evaluates two expressions in it:

// A - direct
const root = await navigator.storage.getDirectory();
const fh = await root.getFileHandle('secret.txt');
return await (await fh.getFile()).text();

// B - through the same-origin SharedWorker
const sw = new SharedWorker('/sw.js', {name:'probe'});
sw.port.start();
const p = new Promise(r => { sw.port.onmessage = e => r(e.data); });
sw.port.postMessage('go');
return await p;

A is the control: same frame, same run.

Result

Four runs, fresh profile and fresh sentinel each:

sentinel=PDFSW-d209fc80058949509648045257982b9d
RESULT  directOPFS=DENIED  viaSharedWorker=GOT-SENTINEL

sentinel=PDFSW-da7f65857aa24684879980669f0d10f5
RESULT  directOPFS=DENIED  viaSharedWorker=GOT-SENTINEL

A returns SecurityError: It was determined that certain files are unsafe for access within a Web application...; B returns the sentinel verbatim. The frame reports location.origin === "http://localhost:8841", self.crossOriginIsolated === false.


The cause

What version of Chrome have you found the security issue in?

155.0.8039.0

No, it is not related to a crash.

Choose the type of vulnerability

Site Isolation Bypass

View on issue tracker