CVE-2026-106271
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/browser/renderer_host/render_frame_host_impl_unittest.cc |
modified | |
RenderFrameHostImplCookieChangeListenerTestcontent/browser/renderer_host/render_frame_host_impl_unittest.cc |
modified | |
IN_PROC_BROWSER_TEST_Fcontent/browser/worker_host/worker_browsertest.cc |
modified |
Files Changed
content/public/renderer/content_renderer_client.htools/metrics/histograms/metadata/stability/enums.xmlcontent/browser/bad_message.hcontent/browser/renderer_host/render_frame_host_impl_unittest.ccthird_party/blink/public/web/web_local_frame_client.hthird_party/blink/renderer/core/frame/local_frame_client_impl.ccchrome/browser/site_isolation/chrome_site_per_process_browsertest.ccchrome/renderer/chrome_content_renderer_client.cccontent/browser/worker_host/worker_browsertest.ccthird_party/blink/renderer/core/loader/empty_clients.hthird_party/blink/renderer/core/workers/dedicated_worker_test.cccontent/browser/renderer_host/render_frame_host_impl.ccthird_party/blink/renderer/core/frame/local_frame_client_impl.hchrome/renderer/chrome_content_renderer_client.hthird_party/blink/renderer/core/workers/dedicated_worker.cccontent/renderer/render_frame_impl.ccthird_party/blink/renderer/core/frame/local_frame_client.hcontent/public/renderer/content_renderer_client.cccontent/renderer/render_frame_impl.h
Patch
From 92608e45cada82a06b513889d567f6f1ce73a2ec Mon Sep 17 00:00:00 2001 From: Nidhi Jaju <[email protected]> Date: Sun, 13 Sep 2026 19:35:30 -0700 Subject: [PATCH] Disallow dedicated worker creation in PDF processes PDF content documents run in isolated renderer processes that do not support or require Web Workers. Allowing dedicated workers to be created from PDF processes permits unnecessary capabilities to be exercised under the PDF's committed origin. This CL tightens PDF process boundaries by: 1. Rejecting DedicatedWorkerHostFactory creation from PDF frames in RenderFrameHostImpl::CreateDedicatedWorkerHostFactory with bad message RFH_DEDICATED_WORKER_HOST_FACTORY_PDF_PROCESS_BLOCKED. 2. Disallowing dedicated worker creation in Blink for PDF documents by throwing a SecurityError DOMException when LocalFrameClient::AreDedicatedWorkersDisabled() is true. This prevents non-compromised renderers (e.g., via extensions or DevTools) from being terminated by the browser-side gate. Bug: 553118313 Change-Id: I46830d061010f7e782c2ec8f13baad89fcb0f1c4 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8374884 Reviewed-by: Andy Phan <[email protected]> Commit-Queue: Will Harris <[email protected]> Reviewed-by: Alex Moshchuk <[email protected]> Reviewed-by: Will Harris <[email protected]> Auto-Submit: Nidhi Jaju <[email protected]> Reviewed-by: Kent Tamura <[email protected]> Cr-Commit-Position: refs/heads/main@{#1697028} --- diff --git a/chrome/renderer/chrome_content_renderer_client.cc b/chrome/renderer/chrome_content_renderer_client.cc index eaa06fb..626ee105 100644 --- a/chrome/renderer/chrome_content_renderer_client.cc +++ b/chrome/renderer/chrome_content_renderer_client.cc @@ -923,6 +923,20 @@ #endif } +bool ChromeContentRendererClient::AreDedicatedWorkersDisabled() const { +#if BUILDFLAG(ENABLE_PDF) && BUILDFLAG(ENABLE_EXTENSIONS) + // PDF renderers shouldn't need to create dedicated workers. Note that it's + // still possible to attempt to instantiate a Worker in a PDF document's + // context via DevTools; returning true here ensures that the constructor + // throws a SecurityError DOMException. This avoids a renderer kill by the + // browser process which isn't expecting PDF renderer processes to ever create + // dedicated workers. See https://crbug.com/553118313. + return pdf::IsPdfRenderer(); +#else + return false; +#endif +} + v8::Local<v8::Object> ChromeContentRendererClient::GetScriptableObject( const blink::WebElement& plugin_element, v8::Isolate* isolate) { diff --git a/chrome/renderer/chrome_content_renderer_client.h b/chrome/renderer/chrome_content_renderer_client.h index 9ac5dba..176157a 100644 --- a/chrome/renderer/chrome_content_renderer_client.h +++ b/chrome/renderer/chrome_content_renderer_client.h @@ -101,6 +101,7 @@ const GURL& original_url, const std::string& mime_type) override; bool IsDomStorageDisabled() const override; + bool AreDedicatedWorkersDisabled() const override; v8::Local<v8::Object> GetScriptableObject( const blink::WebElement& plugin_element, v8::Isolate* isolate) override; diff --git a/content/browser/bad_message.h b/content/browser/bad_message.h index 366b593..af993c2 100644 --- a/content/browser/bad_message.h +++ b/content/browser/bad_message.h @@ -426,6 +426,7 @@ RFPH_SET_IS_INERT_WITHOUT_CPFC = 398, RFPH_SYNCHRONIZE_VISUAL_PROPERTIES_WITHOUT_CPFC = 399, RFPH_UPDATE_VIEWPORT_INTERSECTION_WITHOUT_CPFC = 400, + RFH_DEDICATED_WORKER_HOST_FACTORY_PDF_PROCESS_BLOCKED = 401, // 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/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc index 82b2bbc..13a2167 100644 --- a/content/browser/renderer_host/render_frame_host_impl.cc +++ b/content/browser/renderer_host/render_frame_host_impl.cc @@ -15250,6 +15250,13 @@ void RenderFrameHostImpl::CreateDedicatedWorkerHostFactory( mojo::PendingReceiver<blink::mojom::DedicatedWorkerHostFactory> receiver) { + if (GetSiteInstance()->IsPdf()) { + bad_message::ReceivedBadMessage( + GetProcess(), + bad_message::RFH_DEDICATED_WORKER_HOST_FACTORY_PDF_PROCESS_BLOCKED); + return; + } + // Allocate the worker in the same process as the creator. ChildProcessId worker_process_id = GetProcess()->GetID(); diff --git a/content/public/renderer/content_renderer_client.cc b/content/public/renderer/content_renderer_client.cc index ed752891..86b27f0a 100644 --- a/content/public/renderer/content_renderer_client.cc +++ b/content/public/renderer/content_renderer_client.cc @@ -63,6 +63,10 @@ return false; } +bool ContentRendererClient::AreDedicatedWorkersDisabled() const { + return false; +} + v8::Local<v8::Object> ContentRendererClient::GetScriptableObject( const blink::WebElement& plugin_element, v8::Isolate* isolate) { diff --git a/content/public/renderer/content_renderer_client.h b/content/public/renderer/content_renderer_client.h index 826f710..0b9097d 100644 --- a/content/public/renderer/content_renderer_client.h +++ b/content/public/renderer/content_renderer_client.h @@ -140,6 +140,9 @@ // and sessionStorage. virtual bool IsDomStorageDisabled() const; + // Specifies whether to disable dedicated workers. + virtual bool AreDedicatedWorkersDisabled() const; + // Returns a scriptable object which implements custom javascript API for the // given element. This is used for external plugin handlers for providing // custom API such as|postMessage| for <embed> and <object>. diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc index 93595b3..b844982 100644 --- a/content/renderer/render_frame_impl.cc +++ b/content/renderer/render_frame_impl.cc @@ -3568,6 +3568,10 @@ return GetContentClient()->renderer()->IsDomStorageDisabled(); } +bool RenderFrameImpl::AreDedicatedWorkersDisabled() const { + return GetContentClient()->renderer()->AreDedicatedWorkersDisabled(); +} + v8::Local<v8::Object> RenderFrameImpl::GetScriptableObject( const blink::WebElement& plugin_element, v8::Isolate* isolate) { diff --git a/content/renderer/render_frame_impl.h b/content/renderer/render_frame_impl.h index e4b8998..eb45d9e 100644 --- a/content/renderer/render_frame_impl.h +++ b/content/renderer/render_frame_impl.h @@ -1106,6 +1106,7 @@ const blink::WebURL& url, const blink::WebString& suggested_mime_type) override; bool IsDomStorageDisabled() const override; + bool AreDedicatedWorkersDisabled() const override; v8::Local<v8::Object> GetScriptableObject( const blink::WebElement& plugin_element, v8::Isolate* isolate) override; diff --git a/third_party/blink/public/web/web_local_frame_client.h b/third_party/blink/public/web/web_local_frame_client.h index a0c709c..93ad527 100644 --- a/third_party/blink/public/web/web_local_frame_client.h +++ b/third_party/blink/public/web/web_local_frame_client.h @@ -795,6 +795,9 @@ // and sessionStorage. virtual bool IsDomStorageDisabled() const { return false; } + // Specifies whether to disable dedicated workers. + virtual bool AreDedicatedWorkersDisabled() const { return false; } + // Returns a scriptable object for the given plugin element. This is used for // having an external handler implement certain customized APIs for the // plugin element (e.g., to expose postMessage). diff --git a/third_party/blink/renderer/core/frame/local_frame_client.h b/third_party/blink/renderer/core/frame/local_frame_client.h index efd1f3a..5052635 100644 --- a/third_party/blink/renderer/core/frame/local_frame_client.h +++ b/third_party/blink/renderer/core/frame/local_frame_client.h @@ -447,6 +447,9 @@ // and sessionStorage. virtual bool IsDomStorageDisabled() const { return false; } + // Specifies whether to disable dedicated workers. + virtual bool AreDedicatedWorkersDisabled() const { return false; } + // Debugging ----------------------------------------------------------- virtual void BindDevToolsAgent( mojo::PendingAssociatedRemote<mojom::blink::DevToolsAgentHost> host, diff --git a/third_party/blink/renderer/core/frame/local_frame_client_impl.cc b/third_party/blink/renderer/core/frame/local_frame_client_impl.cc index 81923b4..51e6a9d 100644 --- a/third_party/blink/renderer/core/frame/local_frame_client_impl.cc +++ b/third_party/blink/renderer/core/frame/local_frame_client_impl.cc @@ -1290,6 +1290,10 @@ return web_frame_->Client()->IsDomStorageDisabled(); } +bool LocalFrameClientImpl::AreDedicatedWorkersDisabled() const { + return web_frame_->Client()->AreDedicatedWorkersDisabled(); +} + bool LocalFrameClientImpl::IsForInitialWebUI() const { return web_frame_->Client() && web_frame_->Client()->IsForInitialWebUI(); } diff --git a/third_party/blink/renderer/core/frame/local_frame_client_impl.h b/third_party/blink/renderer/core/frame/local_frame_client_impl.h index 2dc15ac..10d76a2 100644 --- a/third_party/blink/renderer/core/frame/local_frame_client_impl.h +++ b/third_party/blink/renderer/core/frame/local_frame_client_impl.h @@ -305,6 +305,7 @@ override; bool IsDomStorageDisabled() const override; + bool AreDedicatedWorkersDisabled() const override; private: bool IsLocalFrameClientImpl() const override { return true; } diff --git a/third_party/blink/renderer/core/loader/empty_clients.h b/third_party/blink/renderer/core/loader/empty_clients.h index bdaf32a..3cb67eb8 100644 --- a/third_party/blink/renderer/core/loader/empty_clients.h +++ b/third_party/blink/renderer/core/loader/empty_clients.h @@ -494,6 +494,7 @@ } bool IsDomStorageDisabled() const override { return false; } + bool AreDedicatedWorkersDisabled() const override { return false; } protected: // Not owned diff --git a/third_party/blink/renderer/core/workers/dedicated_worker.cc b/third_party/blink/renderer/core/workers/dedicated_worker.cc index f6eef77..fb38a669 100644 --- a/third_party/blink/renderer/core/workers/dedicated_worker.cc +++ b/third_party/blink/renderer/core/workers/dedicated_worker.cc @@ -82,6 +82,15 @@ return nullptr; } + if (auto* window = DynamicTo<LocalDOMWindow>(context)) { + if (window->GetFrame() && window->GetFrame()->Client() && + window->GetFrame()->Client()->AreDedicatedWorkersDisabled()) { + exception_state.ThrowSecurityError( + "Dedicated workers are not supported in this document."); + return nullptr; + } + } + String compliant_url = TrustedTypesCheckForScriptURL( url, context, trusted_types_names::kWorker, trusted_types_names::kConstructor, exception_state); diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml index 8676d9c4e..24c9dfa8 100644 --- a/tools/metrics/histograms/metadata/stability/enums.xml +++ b/tools/metrics/histograms/metadata/stability/enums.xml @@ -583,6 +583,8 @@ <int value="398" label="RFPH_SET_IS_INERT_WITHOUT_CPFC"/> <int value="399" label="RFPH_SYNCHRONIZE_VISUAL_PROPERTIES_WITHOUT_CPFC"/> <int value="400" label="RFPH_UPDATE_VIEWPORT_INTERSECTION_WITHOUT_CPFC"/> + <int value="401" + label="RFH_DEDICATED_WORKER_HOST_FACTORY_PDF_PROCESS_BLOCKED"/> </enum> <enum name="BadMessageReasonExtensions"> diff --git a/chrome/browser/site_isolation/chrome_site_per_process_browsertest.cc b/chrome/browser/site_isolation/chrome_site_per_process_browsertest.cc index 08d59a949..b1116f4 100644 --- a/chrome/browser/site_isolation/chrome_site_per_process_browsertest.cc +++ b/chrome/browser/site_isolation/chrome_site_per_process_browsertest.cc @@ -460,6 +460,45 @@ EXPECT_EQ(base::Value(), content::EvalJs(pdf_frame, "window.sessionStorage")); EXPECT_TRUE(pdf_frame->IsRenderFrameLive()); } + +// Check that navigating to a PDF and then trying to create a dedicated worker +// in the context of the PDF document fails gracefully and doesn't lead to a +// renderer kill. PDF documents don't need dedicated workers, but the creation +// could still be attempted via DevTools. See https://crbug.com/553118313. +IN_PROC_BROWSER_TEST_F(ChromeSitePerProcessOopifPDFTest, + DedicatedWorkersInPDFDocument) { + GURL pdf_url = embedded_test_server()->GetURL("/pdf/test.pdf"); + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), pdf_url)); + content::WebContents* web_contents = + browser()->GetTabStripModel()->GetActiveWebContents(); + EXPECT_EQ(pdf_url, web_contents->GetLastCommittedURL()); + ASSERT_TRUE(GetTestMimeHandlerStreamManager()->WaitUntilPdfLoaded( + web_contents->GetPrimaryMainFrame())); + + // The PDF document should be in the grandchild frame, embedded in the PDF + // viewer extension frame. + content::RenderFrameHost* pdf_extension_frame = + content::ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0); + ASSERT_TRUE(pdf_extension_frame); + content::RenderFrameHost* pdf_frame = + content::ChildFrameAt(pdf_extension_frame, 0); + ASSERT_TRUE(pdf_frame); + EXPECT_TRUE(pdf_frame->GetProcess()->IsPdf()); + + // Attempting to create a dedicated worker from the PDF frame should throw + // a SecurityError DOMException and not lead to a renderer kill. + EXPECT_EQ("SecurityError", + content::EvalJs(pdf_frame, + "(() => {" + " try {" + " new Worker('data:text/javascript,');" + " return 'Success';" + " } catch (e) {" + " return e.name;" + " }" + "})()")); + EXPECT_TRUE(pdf_frame->IsRenderFrameLive()); +} #endif // BUILDFLAG(ENABLE_PDF) // A helper class to verify that a "mailto:" external protocol request succeeds. diff --git a/content/browser/renderer_host/render_frame_host_impl_unittest.cc b/content/browser/renderer_host/render_frame_host_impl_unittest.cc index dc32e80..30097fa 100644 --- a/content/browser/renderer_host/render_frame_host_impl_unittest.cc +++ b/content/browser/renderer_host/render_frame_host_impl_unittest.cc @@ -34,6 +34,7 @@ #include "content/test/test_render_frame_host.h" #include "content/test/test_render_view_host.h" #include "content/test/test_web_contents.h" +#include "mojo/public/cpp/bindings/remote.h" #include "mojo/public/cpp/test_support/fake_message_dispatch_context.h" #include "mojo/public/cpp/test_support/test_utils.h" #include "net/base/features.h" @@ -56,6 +57,7 @@ #include "third_party/blink/public/mojom/favicon/favicon_url.mojom.h" #include "third_party/blink/public/mojom/notifications/notification_service.mojom.h" #include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h" +#include "third_party/blink/public/mojom/worker/dedicated_worker_host_factory.mojom.h" #include "url/gurl.h" #include "url/origin.h" #include "url/url_util.h" @@ -1604,6 +1606,34 @@ EXPECT_EQ(1, process()->bad_msg_count()); } +// Ensure that attempting to create a DedicatedWorkerHostFactory from a PDF +// process causes a bad message. +TEST_F(RenderFrameHostImplTest, DedicatedWorkerBlockedForPdfProcess) { + UrlInfo url_info( + UrlInfoInit(GURL("https://foo.com/document.pdf")) + .WithEmbedderIsolationInfo(EmbedderIsolationInfo::CreateForPdf())); + scoped_refptr<SiteInstanceImpl> pdf_instance = + SiteInstanceImpl::CreateForUrlInfo(GetBrowserContext(), url_info, + /*is_guest=*/false, + /*is_fenced=*/false, + /*is_fixed_storage_partition=*/false); + std::unique_ptr<TestWebContents> pdf_web_contents = + TestWebContents::Create(GetBrowserContext(), pdf_instance); + TestRenderFrameHost* pdf_rfh = pdf_web_contents->GetPrimaryMainFrame(); + + EXPECT_EQ(0, pdf_rfh->GetProcess()->bad_msg_count()); + + base::HistogramTester histograms; + mojo::Remote<blink::mojom::DedicatedWorkerHostFactory> factory; + pdf_rfh->CreateDedicatedWorkerHostFactory( + factory.BindNewPipeAndPassReceiver()); + + EXPECT_EQ(1, pdf_rfh->GetProcess()->bad_msg_count()); + histograms.ExpectUniqueSample( + "Stability.BadMessageTerminated.Content", + bad_message::RFH_DEDICATED_WORKER_HOST_FACTORY_PDF_PROCESS_BLOCKED, 1); +} + class RenderFrameHostImplCookieChangeListenerTest : public RenderFrameHostImplTest, public testing::WithParamInterface<bool> { diff --git a/content/browser/worker_host/worker_browsertest.cc b/content/browser/worker_host/worker_browsertest.cc index 69ab4b7..27e0a1e 100644 --- a/content/browser/worker_host/worker_browsertest.cc +++ b/content/browser/worker_host/worker_browsertest.cc @@ -21,7 +21,9 @@ #include "base/thread_annotations.h" #include "base/threading/thread_restrictions.h" #include "build/build_config.h" +#include "content/browser/bad_message.h" #include "content/browser/process_lock.h" +#include "content/browser/renderer_host/render_frame_host_impl.h" #include "content/browser/security/cpsp/child_process_security_policy_impl.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/browser/worker_host/shared_worker_service_impl.h" @@ -30,6 +32,7 @@ #include "content/public/browser/browser_thread.h" #include "content/public/browser/client_certificate_delegate.h" #include "content/public/browser/dedicated_worker_service.h" +#include "content/public/browser/navigation_controller.h" #include "content/public/browser/shared_worker_service.h" #include "content/public/browser/storage_partition.h" #include "content/public/browser/web_contents.h" @@ -511,6 +514,51 @@ RunTest(GetTestURL("single_worker.html", "shared=true")); } +// A frame committed as PDF content (like the PDF viewer's content frame) runs +// in a process whose SiteInfo has `is_pdf` set. PDF documents never run +// script that could create dedicated workers, so the browser must refuse to +// bind blink.mojom.DedicatedWorkerHostFactory for such processes and +// terminate them instead. +IN_PROC_BROWSER_TEST_F(WorkerTest, DedicatedWorkerBlockedForPdfProcess) { + WebContentsImpl* tab = static_cast<WebContentsImpl*>(shell()->web_contents()); + const GURL url = ssl_server()->GetURL("a.test", "/title1.html"); + + // 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; ... (file diff truncated)
Regression Test / PoC
diff --git a/chrome/browser/site_isolation/chrome_site_per_process_browsertest.cc b/chrome/browser/site_isolation/chrome_site_per_process_browsertest.cc
index 08d59a949..b1116f4 100644
--- a/chrome/browser/site_isolation/chrome_site_per_process_browsertest.cc
+++ b/chrome/browser/site_isolation/chrome_site_per_process_browsertest.cc
@@ -460,6 +460,45 @@
EXPECT_EQ(base::Value(), content::EvalJs(pdf_frame, "window.sessionStorage"));
EXPECT_TRUE(pdf_frame->IsRenderFrameLive());
}
+
+// Check that navigating to a PDF and then trying to create a dedicated worker
+// in the context of the PDF document fails gracefully and doesn't lead to a
+// renderer kill. PDF documents don't need dedicated workers, but the creation
+// could still be attempted via DevTools. See https://crbug.com/553118313.
+IN_PROC_BROWSER_TEST_F(ChromeSitePerProcessOopifPDFTest,
+ DedicatedWorkersInPDFDocument) {
+ GURL pdf_url = embedded_test_server()->GetURL("/pdf/test.pdf");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), pdf_url));
+ content::WebContents* web_contents =
+ browser()->GetTabStripModel()->GetActiveWebContents();
+ EXPECT_EQ(pdf_url, web_contents->GetLastCommittedURL());
+ ASSERT_TRUE(GetTestMimeHandlerStreamManager()->WaitUntilPdfLoaded(
+ web_contents->GetPrimaryMainFrame()));
+
+ // The PDF document should be in the grandchild frame, embedded in the PDF
+ // viewer extension frame.
+ content::RenderFrameHost* pdf_extension_frame =
+ content::ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0);
+ ASSERT_TRUE(pdf_extension_frame);
+ content::RenderFrameHost* pdf_frame =
+ content::ChildFrameAt(pdf_extension_frame, 0);
+ ASSERT_TRUE(pdf_frame);
+ EXPECT_TRUE(pdf_frame->GetProcess()->IsPdf());
+
+ // Attempting to create a dedicated worker from the PDF frame should throw
+ // a SecurityError DOMException and not lead to a renderer kill.
+ EXPECT_EQ("SecurityError",
+ content::EvalJs(pdf_frame,
+ "(() => {"
+ " try {"
+ " new Worker('data:text/javascript,');"
+ " return 'Success';"
+ " } catch (e) {"
+ " return e.name;"
+ " }"
+ "})()"));
+ EXPECT_TRUE(pdf_frame->IsRenderFrameLive());
+}
#endif // BUILDFLAG(ENABLE_PDF)
// A helper class to verify that a "mailto:" external protocol request succeeds.
diff --git a/content/browser/renderer_host/render_frame_host_impl_unittest.cc b/content/browser/renderer_host/render_frame_host_impl_unittest.cc
index dc32e80..30097fa 100644
--- a/content/browser/renderer_host/render_frame_host_impl_unittest.cc
+++ b/content/browser/renderer_host/render_frame_host_impl_unittest.cc
@@ -34,6 +34,7 @@
#include "content/test/test_render_frame_host.h"
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
+#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/test_support/fake_message_dispatch_context.h"
#include "mojo/public/cpp/test_support/test_utils.h"
#include "net/base/features.h"
@@ -56,6 +57,7 @@
#include "third_party/blink/public/mojom/favicon/favicon_url.mojom.h"
#include "third_party/blink/public/mojom/notifications/notification_service.mojom.h"
#include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h"
+#include "third_party/blink/public/mojom/worker/dedicated_worker_host_factory.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_util.h"
@@ -1604,6 +1606,34 @@
EXPECT_EQ(1, process()->bad_msg_count());
}
+// Ensure that attempting to create a DedicatedWorkerHostFactory from a PDF
+// process causes a bad message.
+TEST_F(RenderFrameHostImplTest, DedicatedWorkerBlockedForPdfProcess) {
+ UrlInfo url_info(
+ UrlInfoInit(GURL("https://foo.com/document.pdf"))
+ .WithEmbedderIsolationInfo(EmbedderIsolationInfo::CreateForPdf()));
+ scoped_refptr<SiteInstanceImpl> pdf_instance =
+ SiteInstanceImpl::CreateForUrlInfo(GetBrowserContext(), url_info,
+ /*is_guest=*/false,
+ /*is_fenced=*/false,
+ /*is_fixed_storage_partition=*/false);
+ std::unique_ptr<TestWebContents> pdf_web_contents =
+ TestWebContents::Create(GetBrowserContext(), pdf_instance);
+ TestRenderFrameHost* pdf_rfh = pdf_web_contents->GetPrimaryMainFrame();
+
+ EXPECT_EQ(0, pdf_rfh->GetProcess()->bad_msg_count());
+
+ base::HistogramTester histograms;
+ mojo::Remote<blink::mojom::DedicatedWorkerHostFactory> factory;
+ pdf_rfh->CreateDedicatedWorkerHostFactory(
+ factory.BindNewPipeAndPassReceiver());
+
+ EXPECT_EQ(1, pdf_rfh->GetProcess()->bad_msg_count());
+ histograms.ExpectUniqueSample(
+ "Stability.BadMessageTerminated.Content",
+ bad_message::RFH_DEDICATED_WORKER_HOST_FACTORY_PDF_PROCESS_BLOCKED, 1);
+}
+
class RenderFrameHostImplCookieChangeListenerTest
: public RenderFrameHostImplTest,
public testing::WithParamInterface<bool> {
diff --git a/content/browser/worker_host/worker_browsertest.cc b/content/browser/worker_host/worker_browsertest.cc
index 69ab4b7..27e0a1e 100644
--- a/content/browser/worker_host/worker_browsertest.cc
+++ b/content/browser/worker_host/worker_browsertest.cc
@@ -21,7 +21,9 @@
#include "base/thread_annotations.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
+#include "content/browser/bad_message.h"
#include "content/browser/process_lock.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/security/cpsp/child_process_security_policy_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/worker_host/shared_worker_service_impl.h"
@@ -30,6 +32,7 @@
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/client_certificate_delegate.h"
#include "content/public/browser/dedicated_worker_service.h"
+#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/shared_worker_service.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
@@ -511,6 +514,51 @@
RunTest(GetTestURL("single_worker.html", "shared=true"));
}
+// A frame committed as PDF content (like the PDF viewer's content frame) runs
+// in a process whose SiteInfo has `is_pdf` set. PDF documents never run
+// script that could create dedicated workers, so the browser must refuse to
+// bind blink.mojom.DedicatedWorkerHostFactory for such processes and
+// terminate them instead.
+IN_PROC_BROWSER_TEST_F(WorkerTest, DedicatedWorkerBlockedForPdfProcess) {
+ WebContentsImpl* tab = static_cast<WebContentsImpl*>(shell()->web_contents());
+ const GURL url = ssl_server()->GetURL("a.test", "/title1.html");
+
+ // 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());
+
+ RenderFrameHostImpl* frame = tab->GetPrimaryMainFrame();
+ ASSERT_TRUE(frame->GetSiteInstance()->GetSiteInfo().is_pdf());
+ ASSERT_TRUE(frame->GetProcess()->IsPdf());
+
+ // Attempting to create a dedicated worker from a PDF frame triggers the
+ // renderer to request blink.mojom.DedicatedWorkerHostFactory from
+ // RenderFrameHost. The browser must reject this and terminate the process.
+ RenderProcessHostBadIpcMessageWaiter kill_waiter(frame->GetProcess());
+ ExecuteScriptAsync(frame, "new Worker('/workers/worker_common.js');");
+ EXPECT_EQ(bad_message::RFH_DEDICATED_WORKER_HOST_FACTORY_PDF_PROCESS_BLOCKED,
+ kill_waiter.Wait());
+
+ // Control: the same page committed normally can create dedicated workers.
+ EXPECT_TRUE(NavigateToURL(shell(), url));
+ EXPECT_FALSE(
+ tab->GetPrimaryMainFrame()->GetSiteInstance()->GetSiteInfo().is_pdf());
+ EXPECT_EQ("pong", EvalJs(shell(), R"(
+ new Promise(resolve => {
+ const worker = new Worker('/workers/worker_common.js');
+ worker.onmessage = e => resolve(e.data);
+ worker.postMessage('ping');
+ })
+ )"));
+}
+
// A WebContents created with `disallow_shared_workers` must not be able to
// connect to a shared worker (which would otherwise bridge its process to
// ordinary content of the same origin, since shared worker matching ignores
diff --git a/third_party/blink/renderer/core/workers/dedicated_worker_test.cc b/third_party/blink/renderer/core/workers/dedicated_worker_test.cc
index 74a4341a..252f982 100644
--- a/third_party/blink/renderer/core/workers/dedicated_worker_test.cc
+++ b/third_party/blink/renderer/core/workers/dedicated_worker_test.cc
@@ -26,6 +26,7 @@
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_message_port.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_post_message_options.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_union_trustedscripturl_usvstring.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_worker_options.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/event_type_names.h"
@@ -56,6 +57,7 @@
#include "third_party/blink/renderer/core/workers/worker_or_worklet_global_scope.h"
#include "third_party/blink/renderer/core/workers/worker_thread.h"
#include "third_party/blink/renderer/core/workers/worker_thread_test_helper.h"
+#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/heap/visitor.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_client_settings_object_snapshot.h"
@@ -1215,4 +1217,34 @@
EXPECT_TRUE(force_load_at_top_enabled);
}
+class DedicatedWorkersDisabledLocalFrameClient : public EmptyLocalFrameClient {
+ public:
+ bool AreDedicatedWorkersDisabled() const override { return true; }
+};
+
+class DedicatedWorkerDisabledTest : public PageTestBase {
+ public:
+ void SetUp() override {
+ client_ = MakeGarbageCollected<DedicatedWorkersDisabledLocalFrameClient>();
+ PageTestBase::SetupPageWithClients(nullptr, client_);
+ }
+
+ private:
+ Persistent<DedicatedWorkersDisabledLocalFrameClient> client_;
+};
+
+TEST_F(DedicatedWorkerDisabledTest, DedicatedWorkerDisabledInFrame) {
+ NavigateTo(KURL("https://example.com/"));
+ DummyExceptionStateForTesting exception_state;
+ DedicatedWorker* worker = DedicatedWorker::Create(
+ GetFrame().DomWindow(),
+ MakeGarbageCollected<V8UnionTrustedScriptURLOrUSVString>(
+ "https://example.com/worker.js"),
+ WorkerOptions::Create(), exception_state);
+ EXPECT_FALSE(worker);
+ EXPECT_TRUE(exception_state.HadException());
+ EXPECT_EQ(exception_state.Code(),
+ ToExceptionCode(DOMExceptionCode::kSecurityError));
+}
+
} // namespace blink
Original Bug Report
Site Isolation bypass from PDF renderer via DedicatedWorkerHost BlobURLStore
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: Chromium denies PDF renderers access to blink.mojom.BlobURLStore in frame binders to prevent minting blob URLs under the PDF-serving origin. However, DedicatedWorkerHost::CreateBlobUrlStoreProvider exposes blink.mojom.BlobURLStore on the dedicated worker interface broker without performing an is_pdf() check. A compromised PDF renderer could potentially instantiate a dedicated worker to register a malicious HTML blob URL under the victim origin and commit it in a normal renderer, executing script with the full authority of the serving origin.
Affected files:
content/browser/worker_host/dedicated_worker_host.cccontent/browser/worker_host/dedicated_worker_host_factory_impl.cccontent/browser/browser_interface_binders.cccontent/browser/renderer_host/render_frame_host_impl.cc
Estimated timestamp from git blame: 2026-08-10
Summary
Chromium isolates PDF documents inside dedicated renderer processes tagged with is_pdf = true to ensure untrusted PDF content cannot access the serving origin’s storage, cookies, or DOM. To prevent PDF renderers from minting blob URLs under the serving origin, RenderFrameHostImpl explicitly terminates PDF renderers that attempt to bind blink.mojom.BlobURLStore. However, DedicatedWorkerHost::CreateBlobUrlStoreProvider fails to check whether the process is a PDF process, potentially allowing a compromised PDF renderer to obtain a BlobURLStore and escalate privileges to a full-privilege renderer under the victim origin.
Root Cause
In content/browser/renderer_host/render_frame_host_impl.cc:13753-13758 and 13804-13809, frame and worklet binders explicitly check GetSiteInstance()->GetSiteInfo().is_pdf() and invoke bad_message::ReceivedBadMessage:
if (GetSiteInstance()->GetSiteInfo().is_pdf()) {
bad_message::ReceivedBadMessage(
GetProcess(),
bad_message::RFH_BLOB_URL_STORE_ASSOCIATED_PDF_PROCESS_BLOCKED);
return;
}
In contrast, DedicatedWorkerHost::CreateBlobUrlStoreProvider (content/browser/worker_host/dedicated_worker_host.cc:941-972), which is exposed on the worker’s BrowserInterfaceBroker (content/browser/browser_interface_binders.cc:1568-1570), omits this check:
void DedicatedWorkerHost::CreateBlobUrlStoreProvider(
mojo::PendingReceiver<blink::mojom::BlobURLStore> receiver) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
auto* storage_partition_impl = GetStoragePartitionImpl();
storage_partition_impl->GetBlobUrlRegistry()->AddReceiver(
GetWorkerStorageKey(), renderer_origin_,
GetProcessHost()->GetDeprecatedID(), std::move(receiver), ...);
}
Because RenderFrameHostImpl::CreateDedicatedWorkerHostFactory (content/browser/renderer_host/render_frame_host_impl.cc:15219-15238) is bound without a PDF check and creates the worker in the creator’s process, a PDF renderer can create a dedicated worker and bind blink.mojom.BlobURLStore for the PDF’s committed origin.
Potential Trigger Steps
(Note: These are potential steps based on static code analysis, as our tooling environment does not currently have the capability to execute code).
- A victim user opens an untrusted PDF hosted inline on
https://victim.example/doc.pdf. The document commits in an isolated renderer withis_pdf() == trueand storage key originhttps://victim.example. - The attacker compromises the PDF renderer process (e.g., via a memory safety bug in PDFium).
- From the compromised process, the attacker requests
blink.mojom.DedicatedWorkerHostFactoryon the frame’sBrowserInterfaceBroker. - The attacker invokes
DedicatedWorkerHostFactory::CreateWorkerHostAndStartScriptLoadwithscript_url = https://victim.example/doc.pdf. The browser binds the worker’sBrowserInterfaceBrokerand delivers it to the renderer viaOnWorkerHostCreated. - The attacker requests
blink.mojom.BlobURLStoreon the worker’sBrowserInterfaceBroker, bypassing the frame-levelis_pdfbad message check. - The attacker registers a bytes-backed
Blobcontaining HTML/JS payload viablink.mojom.BlobRegistry, and registers it asblob:https://victim.example/<uuid>on the worker’sBlobURLStore. - The attacker calls
BlobURLStore::ResolveAsBlobURLTokenwithis_top_level_navigation = trueto obtain aBlobURLToken. - The attacker initiates a top-level navigation via
RenderFrameProxyHost::OpenURLwithurl = blob:https://victim.example/<uuid>and the token. - The browser validates the request and navigates the top-level frame. Because the destination is a blob document,
embedder_isolation_info.is_pdf()is false, and the page commits in a regular, full-privilege renderer underhttps://victim.example.
Security Impact
This vulnerability allows a compromised PDF renderer to bypass PDF process isolation, resulting in arbitrary JavaScript execution in the context of the PDF-serving origin (Universal XSS / Site Isolation bypass). The script can access unpartitioned storage, cookies, and authenticated network APIs for that origin.
Suggested Fix
- In
DedicatedWorkerHost::CreateBlobUrlStoreProvider(content/browser/worker_host/dedicated_worker_host.cc:941), verify whether the host’s process lock or site info hasis_pdf()set and terminate the process with a bad message if true (mirroringRenderFrameHostImpl). - As defense in depth, refuse creation of
DedicatedWorkerHostFactoryinRenderFrameHostImpl::CreateDedicatedWorkerHostFactoryifGetSiteInstance()->GetSiteInfo().is_pdf()is true, since PDF documents do not require worker support.
Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.