CVE-2026-113270
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/browser/worker_host/shared_worker_service_impl_unittest.cc |
modified | |
PdfWorkerTestcontent/browser/worker_host/worker_browsertest.cc |
modified | |
PdfWorkerTestcontent/browser/worker_host/worker_browsertest.cc |
modified | |
IN_PROC_BROWSER_TEST_Fcontent/browser/worker_host/worker_browsertest.cc |
modified |
Files Changed
content/browser/worker_host/worker_browsertest.cccontent/browser/worker_host/shared_worker_service_impl_unittest.cccontent/browser/worker_host/shared_worker_service_impl.cc
Patch
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())
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 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())
Original Bug Report
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 theCanAccessDataForOrigin()check above so a compromised PDF renderer cannot reach persistent storage directly.CL 8178927added thedisallow_shared_workerscheck above, in this same function, forWebContents::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 touchConnectToWorker.
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 OPFSsecret.txt, then startsnew SharedWorker('/sw.js', {name:'probe'})./sw.jsreads that file on request and posts it back through its port./mid.html(B) embeds/seed.html./embed.html(A, top level) embeds/mid.htmland/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
Is the security issue related to a crash?
No, it is not related to a crash.
Choose the type of vulnerability
Site Isolation Bypass