CVE-2026-79267
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/worker_host/dedicated_worker_host.cc |
modified |
Files Changed
content/browser/browser_interface_binders.cccontent/browser/compute_pressure/pressure_service_for_dedicated_worker.cccontent/browser/compute_pressure/pressure_service_for_worker_unittest.cccontent/browser/renderer_host/render_frame_host_impl.cccontent/browser/worker_host/dedicated_worker_host.cc
Patch
From bde3d8e65bd0936d94c1e71fa9f9363481626ef6 Mon Sep 17 00:00:00 2001
From: Yoshisto Yanagisawa <yyanagisawa@chromium.org>
Date: Mon, 06 Jul 2026 23:06:21 -0700
Subject: [PATCH] [worker] Bind DedicatedWorkerHost to its creator document
DedicatedWorkerHost resolved its owning frame purely by
GlobalRenderFrameHostId on every capability request. When a RenderFrameHost
is reused for a same-site navigation that id resolves to a different
document, so the worker could consult the new document's permissions policy
and isolation info instead of the document that created it.
Record the ancestor frame's DocumentToken when the host (and its factory)
is created and add GetAncestorRenderFrameHost(), which only returns the
frame while it still hosts that document. All call sites, including the
WebNN binder and the compute-pressure service, now go through this helper
so requests are dropped once the creator document is gone, regardless of
the RenderDocument level.
TAG=agy
CONV=f7ad542f-e336-41b5-945a-a9878047820e
Bug: 497839983
Change-Id: I652a36a575f69dcf62006512f3e3990d6dd41475
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8017904
Reviewed-by: Hidehiko Abe <hidehiko@chromium.org>
Commit-Queue: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1657696}
---
diff --git a/content/browser/browser_interface_binders.cc b/content/browser/browser_interface_binders.cc
index 03864a2..73a3181 100644
--- a/content/browser/browser_interface_binders.cc
+++ b/content/browser/browser_interface_binders.cc
@@ -329,8 +329,7 @@
template <typename WorkerHost>
bool IsWebNNPermissionsPolicyBlocked(WorkerHost* host) {
if constexpr (std::is_same_v<WorkerHost, DedicatedWorkerHost>) {
- auto* ancestor_render_frame_host =
- RenderFrameHostImpl::FromID(host->GetAncestorRenderFrameHostId());
+ auto* ancestor_render_frame_host = host->GetAncestorRenderFrameHost();
return !ancestor_render_frame_host ||
!ancestor_render_frame_host->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kWebNN);
diff --git a/content/browser/compute_pressure/pressure_service_for_dedicated_worker.cc b/content/browser/compute_pressure/pressure_service_for_dedicated_worker.cc
index 67fec718..1f9e32d 100644
--- a/content/browser/compute_pressure/pressure_service_for_dedicated_worker.cc
+++ b/content/browser/compute_pressure/pressure_service_for_dedicated_worker.cc
@@ -25,9 +25,7 @@
// https://www.w3.org/TR/compute-pressure/#dfn-owning-document-set
// https://www.w3.org/TR/compute-pressure/#dfn-may-receive-data
- auto* rfh =
- RenderFrameHostImpl::FromID(worker_host_->GetAncestorRenderFrameHostId());
- return HasImplicitFocus(rfh);
+ return HasImplicitFocus(worker_host_->GetAncestorRenderFrameHost());
}
std::optional<base::UnguessableToken>
@@ -35,9 +33,8 @@
device::mojom::PressureSource source) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
- const auto* web_contents =
- WebContents::FromRenderFrameHost(RenderFrameHostImpl::FromID(
- worker_host_->GetAncestorRenderFrameHostId()));
+ const auto* web_contents = WebContents::FromRenderFrameHost(
+ worker_host_->GetAncestorRenderFrameHost());
if (const auto* pressure_manager_proxy =
WebContentsPressureManagerProxy::FromWebContents(web_contents)) {
return pressure_manager_proxy->GetTokenFor(source);
@@ -48,8 +45,7 @@
RenderFrameHost* PressureServiceForDedicatedWorker::GetRenderFrameHost() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
- return RenderFrameHostImpl::FromID(
- worker_host_->GetAncestorRenderFrameHostId());
+ return worker_host_->GetAncestorRenderFrameHost();
}
} // namespace content
diff --git a/content/browser/compute_pressure/pressure_service_for_worker_unittest.cc b/content/browser/compute_pressure/pressure_service_for_worker_unittest.cc
index c33a0461..cf70bdc 100644
--- a/content/browser/compute_pressure/pressure_service_for_worker_unittest.cc
+++ b/content/browser/compute_pressure/pressure_service_for_worker_unittest.cc
@@ -20,6 +20,7 @@
#include "content/browser/worker_host/shared_worker_service_impl.h"
#include "content/browser/worker_host/worker_util.h"
#include "content/public/browser/shared_worker_instance.h"
+#include "content/public/browser/weak_document_ptr.h"
#include "content/public/test/navigation_simulator.h"
#include "content/test/test_render_frame_host.h"
#include "content/test/test_render_view_host.h"
@@ -147,9 +148,10 @@
CHECK_EQ(rfh->GetLastCommittedOrigin(), rfh->GetStorageKey().origin());
worker_host_ = std::make_unique<DedicatedWorkerHost>(
&worker_service_, blink::DedicatedWorkerToken(), rfh->GetProcess(),
- rfh->GetGlobalId(), rfh->GetGlobalId(), rfh->GetStorageKey().origin(),
- rfh->GetStorageKey(), rfh->GetStorageKey().origin(),
- rfh->GetIsolationInfoForSubresources(), rfh->BuildClientSecurityState(),
+ rfh->GetGlobalId(), rfh->GetWeakDocumentPtr(),
+ rfh->GetStorageKey().origin(), rfh->GetStorageKey(),
+ rfh->GetStorageKey().origin(), rfh->GetIsolationInfoForSubresources(),
+ rfh->BuildClientSecurityState(),
rfh->policy_container_host()->policies(),
/*creator_coep_reporter=*/nullptr,
network::GetTestNetworkRestrictionsId(),
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 43c02647..5854915 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -14877,8 +14877,7 @@
mojo::MakeSelfOwnedReceiver(
std::make_unique<DedicatedWorkerHostFactoryImpl>(
worker_process_id,
- /*creator=*/GetGlobalId(),
- /*ancestor_render_frame_host_id=*/GetGlobalId(), GetStorageKey(),
+ /*creator=*/GetGlobalId(), GetWeakDocumentPtr(), GetStorageKey(),
isolation_info_, BuildClientSecurityState(),
policy_container_host()->policies(),
/*creator_coep_reporter=*/coep_reporter, GetNetworkRestrictionsID()),
diff --git a/content/browser/worker_host/dedicated_worker_host.cc b/content/browser/worker_host/dedicated_worker_host.cc
index fceaa928..656b953 100644
--- a/content/browser/worker_host/dedicated_worker_host.cc
+++ b/content/browser/worker_host/dedicated_worker_host.cc
@@ -80,7 +80,7 @@
const blink::DedicatedWorkerToken& token,
RenderProcessHost* worker_process_host,
DedicatedWorkerCreator creator,
- GlobalRenderFrameHostId ancestor_render_frame_host_id,
+ WeakDocumentPtr ancestor_document,
const url::Origin& creator_origin,
const blink::StorageKey& worker_storage_key,
const url::Origin& renderer_origin,
@@ -95,7 +95,7 @@
token_(token),
worker_process_host_(worker_process_host),
creator_(creator),
- ancestor_render_frame_host_id_(ancestor_render_frame_host_id),
+ ancestor_document_(std::move(ancestor_document)),
creator_origin_(creator_origin),
worker_storage_key_(worker_storage_key),
renderer_origin_(renderer_origin),
@@ -129,8 +129,7 @@
service_->NotifyWorkerCreated(this);
- auto* ancestor_render_frame_host =
- RenderFrameHostImpl::FromID(ancestor_render_frame_host_id_);
+ auto* ancestor_render_frame_host = GetAncestorRenderFrameHost();
if (ancestor_render_frame_host) {
DedicatedWorkerHostsForDocument::GetOrCreateForCurrentDocument(
ancestor_render_frame_host)
@@ -156,13 +155,7 @@
// or RenderProcessHostObserver. This destruction should be called before
// the observed render process host (`worker_process_host_`) is destroyed.
- // The frame's current document might no longer be related to this worker. In
- // this case, the previous DedicatedWorkerHostsForDocument has been deleted
- // and calling Remove(...)` on the new one is a no-op. Note that when the
- // previous document is BFCached and not deleted, the RenderFrameHost will
- // never be reused, so we will always get the right (BFCached) document.
- auto* ancestor_render_frame_host =
- RenderFrameHostImpl::FromID(ancestor_render_frame_host_id_);
+ auto* ancestor_render_frame_host = GetAncestorRenderFrameHost();
if (ancestor_render_frame_host) {
DedicatedWorkerHostsForDocument::GetOrCreateForCurrentDocument(
ancestor_render_frame_host)
@@ -189,6 +182,17 @@
WorkerDevToolsManager::GetInstance().WorkerDestroyed(this);
}
+GlobalRenderFrameHostId DedicatedWorkerHost::GetAncestorRenderFrameHostId()
+ const {
+ RenderFrameHost* rfh = ancestor_document_.AsRenderFrameHostIfValid();
+ return rfh ? rfh->GetGlobalId() : GlobalRenderFrameHostId();
+}
+
+RenderFrameHostImpl* DedicatedWorkerHost::GetAncestorRenderFrameHost() const {
+ return RenderFrameHostImpl::From(
+ ancestor_document_.AsRenderFrameHostIfValid());
+}
+
void DedicatedWorkerHost::BindBrowserInterfaceBrokerReceiver(
mojo::PendingReceiver<blink::mojom::BrowserInterfaceBroker> receiver) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
@@ -201,16 +205,15 @@
void DedicatedWorkerHost::CreateContentSecurityNotifier(
mojo::PendingReceiver<blink::mojom::ContentSecurityNotifier> receiver) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
- auto* ancestor_render_frame_host =
- RenderFrameHostImpl::FromID(ancestor_render_frame_host_id_);
+ auto* ancestor_render_frame_host = GetAncestorRenderFrameHost();
if (!ancestor_render_frame_host) {
// The ancestor frame may have already been closed. In that case, the worker
// will soon be terminated too, so abort the connection.
return;
}
- mojo::MakeSelfOwnedReceiver(
- std::make_unique<ContentSecurityNotifier>(ancestor_render_frame_host_id_),
Regression Test / PoC
diff --git a/content/browser/compute_pressure/pressure_service_for_worker_unittest.cc b/content/browser/compute_pressure/pressure_service_for_worker_unittest.cc
index c33a0461..cf70bdc 100644
--- a/content/browser/compute_pressure/pressure_service_for_worker_unittest.cc
+++ b/content/browser/compute_pressure/pressure_service_for_worker_unittest.cc
@@ -20,6 +20,7 @@
#include "content/browser/worker_host/shared_worker_service_impl.h"
#include "content/browser/worker_host/worker_util.h"
#include "content/public/browser/shared_worker_instance.h"
+#include "content/public/browser/weak_document_ptr.h"
#include "content/public/test/navigation_simulator.h"
#include "content/test/test_render_frame_host.h"
#include "content/test/test_render_view_host.h"
@@ -147,9 +148,10 @@
CHECK_EQ(rfh->GetLastCommittedOrigin(), rfh->GetStorageKey().origin());
worker_host_ = std::make_unique<DedicatedWorkerHost>(
&worker_service_, blink::DedicatedWorkerToken(), rfh->GetProcess(),
- rfh->GetGlobalId(), rfh->GetGlobalId(), rfh->GetStorageKey().origin(),
- rfh->GetStorageKey(), rfh->GetStorageKey().origin(),
- rfh->GetIsolationInfoForSubresources(), rfh->BuildClientSecurityState(),
+ rfh->GetGlobalId(), rfh->GetWeakDocumentPtr(),
+ rfh->GetStorageKey().origin(), rfh->GetStorageKey(),
+ rfh->GetStorageKey().origin(), rfh->GetIsolationInfoForSubresources(),
+ rfh->BuildClientSecurityState(),
rfh->policy_container_host()->policies(),
/*creator_coep_reporter=*/nullptr,
network::GetTestNetworkRestrictionsId(),
diff --git a/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc b/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
index 63329114..4b99649 100644
--- a/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
+++ b/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
@@ -11,14 +11,19 @@
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/test/scoped_feature_list.h"
+#include "components/services/storage/privileged/cpp/bucket_client_info.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/site_instance_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/browser/worker_host/dedicated_worker_host.h"
#include "content/browser/worker_host/dedicated_worker_host_factory_impl.h"
+#include "content/common/content_navigation_policy.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/content_features.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/render_document_feature.h"
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
#include "mojo/public/cpp/bindings/remote.h"
@@ -60,7 +65,9 @@
mojo::MakeSelfOwnedReceiver(
std::make_unique<DedicatedWorkerHostFactoryImpl>(
worker_process_id, /*creator=*/render_frame_host_id,
- render_frame_host_id, blink::StorageKey::CreateFirstParty(origin),
+ RenderFrameHostImpl::FromID(render_frame_host_id)
+ ->GetWeakDocumentPtr(),
+ blink::StorageKey::CreateFirstParty(origin),
net::IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
network::mojom::ClientSecurityState::New(),
PolicyContainerPolicies(), coep_reporter->GetWeakPtr(),
@@ -290,6 +297,71 @@
EXPECT_TRUE(observer.dedicated_worker_infos().empty());
}
+class DedicatedWorkerHostNavigationTest
+ : public DedicatedWorkerServiceImplTest {
+ public:
+ DedicatedWorkerHostNavigationTest() {
+ // Allow same-site navigations to reuse the RenderFrameHost so that the
+ // ancestor frame id can resolve to a different document.
+ InitAndEnableRenderDocumentFeature(
+ &feature_list_,
+ GetRenderDocumentLevelName(RenderDocumentLevel::kCrashedFrame));
+ }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+TEST_F(DedicatedWorkerHostNavigationTest,
+ AncestorLookupAfterSameSiteNavigation) {
+ TestDedicatedWorkerServiceObserver observer;
+ base::ScopedObservation<DedicatedWorkerService,
+ DedicatedWorkerService::Observer>
+ scoped_observation(&observer);
+ scoped_observation.Observe(GetDedicatedWorkerService());
+
+ const GURL kUrlA("http://example.com/a.html");
+ std::unique_ptr<TestWebContents> web_contents = CreateWebContents(kUrlA);
+ RenderFrameHostImpl* render_frame_host = web_contents->GetPrimaryMainFrame();
+ const blink::DocumentToken creator_document_token =
+ render_frame_host->GetDocumentToken();
+
+ // Create a dedicated worker for the current document.
+ auto mock_dedicated_worker = std::make_unique<MockDedicatedWorker>(
+ render_frame_host->GetProcess()->GetID(),
+ render_frame_host->GetGlobalId(), url::Origin::Create(kUrlA));
+ observer.RunUntilWorkerEvent();
+ ASSERT_EQ(observer.dedicated_worker_infos().size(), 1u);
+ const blink::DedicatedWorkerToken worker_token =
+ observer.dedicated_worker_infos().begin()->first;
+ DedicatedWorkerHost* host =
+ static_cast<DedicatedWorkerServiceImpl*>(GetDedicatedWorkerService())
+ ->GetDedicatedWorkerHostFromToken(worker_token);
+ ASSERT_TRUE(host);
+ EXPECT_EQ(host->GetAncestorRenderFrameHost(), render_frame_host);
+ EXPECT_EQ(host->GetBucketClientInfo().document_token, creator_document_token);
+
+ // Commit a same-site navigation that reuses the RenderFrameHost so that it
+ // hosts a different document.
+ DisableProactiveBrowsingInstanceSwapFor(render_frame_host);
+ const GURL kUrlB("http://example.com/b.html");
+ NavigationSimulator::NavigateAndCommitFromDocument(kUrlB, render_frame_host);
+ ASSERT_EQ(web_contents->GetPrimaryMainFrame(), render_frame_host);
+ ASSERT_NE(render_frame_host->GetDocumentToken(), creator_document_token);
+ ASSERT_TRUE(
+ static_cast<DedicatedWorkerServiceImpl*>(GetDedicatedWorkerService())
+ ->GetDedicatedWorkerHostFromToken(worker_token));
+
+ // The worker host must not resolve its ancestor to the document that
+ // replaced its creator.
+ EXPECT_FALSE(host->GetAncestorRenderFrameHost());
+ EXPECT_NE(host->GetBucketClientInfo().document_token,
+ render_frame_host->GetDocumentToken());
+
+ mock_dedicated_worker = nullptr;
+ observer.RunUntilWorkerEvent();
+}
+
class DedicatedWorkerHostFactoryImplTest
: public RenderViewHostImplTestHarness {
public:
@@ -332,7 +404,8 @@
return std::make_unique<DedicatedWorkerHostFactoryImpl>(
creator_rfh->GetProcess()->GetID(),
static_cast<RenderFrameHostImpl*>(creator_rfh)->GetGlobalId(),
- static_cast<RenderFrameHostImpl*>(creator_rfh)->GetGlobalId(),
+ static_cast<RenderFrameHostImpl*>(creator_rfh)
+ ->GetWeakDocumentPtr(),
blink::StorageKey::CreateFirstParty(creator_origin),
net::IsolationInfo::CreateTransient(std::nullopt),
network::mojom::ClientSecurityState::New(),
@@ -483,7 +556,8 @@
return std::make_unique<DedicatedWorkerHostFactoryImpl>(
opaque_rfh->GetProcess()->GetID(),
static_cast<RenderFrameHostImpl*>(opaque_rfh)->GetGlobalId(),
- static_cast<RenderFrameHostImpl*>(opaque_rfh)->GetGlobalId(),
+ static_cast<RenderFrameHostImpl*>(opaque_rfh)
+ ->GetWeakDocumentPtr(),
blink::StorageKey::CreateFirstParty(kOpaqueOrigin),
net::IsolationInfo::CreateTransient(std::nullopt),
network::mojom::ClientSecurityState::New(),
Original Bug Report
Potential TOCTOU in DedicatedWorkerHost allows cross-document capability delegation
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: DedicatedWorkerHost performs live lookups of its ancestor RenderFrameHost using a GlobalRenderFrameHostId. Because RenderFrameHosts can be reused across same-site navigations, a worker from a previous document can inadvertently access capabilities or network contexts belonging to a newly navigated document.
Affected files:
content/browser/worker_host/dedicated_worker_host.cccontent/browser/worker_host/dedicated_worker_host.h
Estimated timestamp from git blame: 2024-09-18
Conclusion: A Time-of-Check Time-of-Use (TOCTOU) vulnerability exists in DedicatedWorkerHost, enabling Cross-Origin/Same-Origin Policy bypasses via cross-document capability delegation.
Analysis:
Initial logic and architecture parameters are validated: DedicatedWorkerHost retains a GlobalRenderFrameHostId (ancestor_render_frame_host_id_) to its creator frame. It performs live lookups via RenderFrameHostImpl::FromID() to authorize capabilities like WebUSB, Direct Sockets, or subresource loading. RenderFrameHosts (RFHs) are frequently reused across same-site navigations, meaning the ID remains constant while the underlying Document, Origin, and PermissionsPolicy fundamentally change.
The vulnerability manifests during this RFH reuse. If a dedicated worker outlives its creating document (e.g., via a compromised renderer delaying Mojo pipe teardown, or an IPC race condition), subsequent capability requests are evaluated against the new document’s state.
Potential Attack Steps (Theoretical): Note: These are suggested steps; our tooling agent does not run code to provide a working PoC.
- A compromised renderer loads Document A (e.g.,
https://a.example.com,Permissions-Policy: usb=()) in an iframe and spawns a DedicatedWorker. - The renderer initiates a same-site navigation in the iframe to Document B (e.g.,
https://b.example.com,Permissions-Policy: usb=*). - The browser reuses the existing
RenderFrameHost, updating its policy and origin to Document B. Document A’sDocumentUserDatais destroyed, but the browser does not actively terminate theDedicatedWorkerHostas the renderer intentionally holds the Mojo pipe open. - The worker from Document A sends an IPC requesting
WebUsbService(or triggers a network factory rebuild via a simulated NetworkService crash). DedicatedWorkerHost::CreateWebUsbServicelooks up the RFH ID, retrieves Document B’s RFH, and validates the request against Document B’s permissive policy.- The browser incorrectly grants WebUSB access (or Document B’s network context/cookies) to Document A’s worker.
Impacted Functions:
DedicatedWorkerHost::CreateWebUsbServiceDedicatedWorkerHost::CreateWebSocketConnectorDedicatedWorkerHost::CreateDirectSocketsServiceDedicatedWorkerHost::CreateNetworkFactoryForSubresources(leaksIsolationInfoand cookie overrides)
Suggested Fix:
Modify DedicatedWorkerHost to store a blink::DocumentToken alongside the GlobalRenderFrameHostId at creation. During live lookups, verify that ancestor_render_frame_host->GetDocumentToken() matches the stored token. If they differ, the capability request must be safely aborted.
Evaluated with Chrome root at commit: 4859e669de60239572397b559c17dfab074511e9
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.