Medium chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Workers
DescriptionMissing authorization in Workers
ComponentWorkers
Sub-componentBlink>Workers, Internals>Sandbox>SiteIsolation
Bug ClassAuthorization Error
Tracker553118313
Fix commit92608e45cada (chromium/src) +207/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
TEST_F
content/browser/renderer_host/render_frame_host_impl_unittest.cc
modified
RenderFrameHostImplCookieChangeListenerTest
content/browser/renderer_host/render_frame_host_impl_unittest.cc
modified
IN_PROC_BROWSER_TEST_F
content/browser/worker_host/worker_browsertest.cc
modified

Files Changed

  • content/public/renderer/content_renderer_client.h
  • tools/metrics/histograms/metadata/stability/enums.xml
  • content/browser/bad_message.h
  • content/browser/renderer_host/render_frame_host_impl_unittest.cc
  • third_party/blink/public/web/web_local_frame_client.h
  • third_party/blink/renderer/core/frame/local_frame_client_impl.cc
  • chrome/browser/site_isolation/chrome_site_per_process_browsertest.cc
  • chrome/renderer/chrome_content_renderer_client.cc
  • content/browser/worker_host/worker_browsertest.cc
  • third_party/blink/renderer/core/loader/empty_clients.h
  • third_party/blink/renderer/core/workers/dedicated_worker_test.cc
  • content/browser/renderer_host/render_frame_host_impl.cc
  • third_party/blink/renderer/core/frame/local_frame_client_impl.h
  • chrome/renderer/chrome_content_renderer_client.h
  • third_party/blink/renderer/core/workers/dedicated_worker.cc
  • content/renderer/render_frame_impl.cc
  • third_party/blink/renderer/core/frame/local_frame_client.h
  • content/public/renderer/content_renderer_client.cc
  • content/renderer/render_frame_impl.h
From 92608e45cada82a06b513889d567f6f1ce73a2ec Mon Sep 17 00:00:00 2001
From: Nidhi Jaju <nidhijaju@chromium.org>
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 <andyphan@chromium.org>
Commit-Queue: Will Harris <wfh@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Reviewed-by: Will Harris <wfh@chromium.org>
Auto-Submit: Nidhi Jaju <nidhijaju@chromium.org>
Reviewed-by: Kent Tamura <tkent@chromium.org>
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)
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by vm...@google.com

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.cc
  • content/browser/worker_host/dedicated_worker_host_factory_impl.cc
  • content/browser/browser_interface_binders.cc
  • content/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).

  1. A victim user opens an untrusted PDF hosted inline on https://victim.example/doc.pdf. The document commits in an isolated renderer with is_pdf() == true and storage key origin https://victim.example.
  2. The attacker compromises the PDF renderer process (e.g., via a memory safety bug in PDFium).
  3. From the compromised process, the attacker requests blink.mojom.DedicatedWorkerHostFactory on the frame’s BrowserInterfaceBroker.
  4. The attacker invokes DedicatedWorkerHostFactory::CreateWorkerHostAndStartScriptLoad with script_url = https://victim.example/doc.pdf. The browser binds the worker’s BrowserInterfaceBroker and delivers it to the renderer via OnWorkerHostCreated.
  5. The attacker requests blink.mojom.BlobURLStore on the worker’s BrowserInterfaceBroker, bypassing the frame-level is_pdf bad message check.
  6. The attacker registers a bytes-backed Blob containing HTML/JS payload via blink.mojom.BlobRegistry, and registers it as blob:https://victim.example/<uuid> on the worker’s BlobURLStore.
  7. The attacker calls BlobURLStore::ResolveAsBlobURLToken with is_top_level_navigation = true to obtain a BlobURLToken.
  8. The attacker initiates a top-level navigation via RenderFrameProxyHost::OpenURL with url = blob:https://victim.example/<uuid> and the token.
  9. 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 under https://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

  1. In DedicatedWorkerHost::CreateBlobUrlStoreProvider (content/browser/worker_host/dedicated_worker_host.cc:941), verify whether the host’s process lock or site info has is_pdf() set and terminate the process with a bad message if true (mirroring RenderFrameHostImpl).
  2. As defense in depth, refuse creation of DedicatedWorkerHostFactory in RenderFrameHostImpl::CreateDedicatedWorkerHostFactory if GetSiteInstance()->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.

View on issue tracker