CVE-2026-93378
Overview
Background
- BlobURLStore
- the browser-side
blink.mojom.BlobURLStoreMojo interface that a renderer binds to register, look up, and revokeblob:URLs for its origin. - PDF process
- a locked, privilege-reduced renderer process (its
SiteInfohasis_pdf()set andRenderProcessHost::IsPdf()returns true) that hosts untrusted PDF content and is meant to have no access to origin-scoped storage. - `bad_message::ReceivedBadMessage`
- the browser-process routine that records a
bad_messagereason and terminates a renderer that sent an IPC it should never have been able to send. - `IsDomStorageDisabled()`
- a
LocalFrameClientpredicate (true in PDF frames) that the renderer uses to decide, in thechrome/layer, that DOM storage and blob URLs are unavailable for the frame.
Root Cause Analysis
The browser-process binders RenderFrameHostImpl::BindBlobUrlStoreAssociatedReceiver (used by frames) and RenderFrameHostImpl::BindBlobUrlStoreReceiver (used by worklets) unconditionally handed out a BlobURLStore to any requesting renderer, including a PDF process whose SiteInstance is marked is_pdf(). This violated the invariant that a PDF renderer must never create or access blob URLs for the origin that served the PDF, so a PDF process (which is meant to be storage-isolated) could bind the interface and register/resolve blob URLs.
The fix adds, at the top of both binders and gated by the new kEnforcePdfBlobRestrictions killswitch, a check that calls bad_message::ReceivedBadMessage and returns before any binding when GetSiteInstance()->GetSiteInfo().is_pdf() is true, closing the browser-side authorization gap. Because opaque-origin sandboxed frames may legitimately create null-origin blob URLs, CanAccessDataForOrigin() could not be used, so the check keys specifically on is_pdf(). The complementary renderer-side changes (stopping PublicURLManager and returning a null remote from LocalFrame::GetBlobUrlStorePendingRemote) make a non-compromised PDF renderer fail blob creation gracefully — returning an empty string from createObjectURL — instead of tripping the new browser-side kill.
BlobURLStore for PDF processes that should never have blob-URL capability. The fix adds an is_pdf() guard in both browser-side binders that rejects the request and terminates the process before any receiver is bound.Attack Path
- Obtain a PDF renderer context
Load a PDF so that content runs in a process whose
SiteInfoisis_pdf()andRenderProcessHost::IsPdf()is true. - Trigger the interface request
From that context (e.g. via
URL.createObjectURLreached through DevTools, an extension, or a compromised renderer), cause the renderer to request binding ofblink.mojom.BlobURLStorethrough the associated frame interface or the worklet interface broker. - Browser binds without checking (pre-patch)
BindBlobUrlStoreAssociatedReceiver/BindBlobUrlStoreReceiverbound the receiver with nois_pdf()check, granting the storage-isolated PDF process a workingBlobURLStore. - Register/resolve blob URLs from an isolated process With the bound store, the PDF process can register and look up blob URLs, gaining origin-scoped storage capability it was never meant to have (inferred: any further access to or cross-origin exposure of data via those blob URLs).
Impact Assessment
BlobURLStore and create/resolve blob URLs, defeating the storage isolation that PDF processes are supposed to enforce (a missing-authorization / logic error in Storage). Exploitation requires executing script in a PDF process context, and any escalation beyond obtaining blob-URL storage access — such as reading or exfiltrating origin-scoped data — is (inferred) and not demonstrated by the patch. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 3.1.Changed Functions
| Function | Change | Notes |
|---|---|---|
worker_url_store_third_party/blink/renderer/core/fileapi/public_url_manager.cc |
modified | |
ifthird_party/blink/renderer/core/fileapi/public_url_manager.cc |
modified | |
GetBlobUrlStorePendingRemotethird_party/blink/renderer/core/frame/local_frame.cc |
modified | |
TakeBlobUrlStorePendingRemotethird_party/blink/renderer/core/workers/worklet_global_scope.cc |
modified | |
IN_PROC_BROWSER_TEST_Fcontent/browser/blob_storage/blob_url_browsertest.cc |
modified |
Files Changed
third_party/blink/renderer/core/fileapi/public_url_manager_test.cccontent/browser/bad_message.htools/metrics/histograms/metadata/stability/enums.xmlthird_party/blink/common/features.ccthird_party/blink/public/common/features.hcontent/browser/blob_storage/blob_url_browsertest.ccchrome/browser/site_isolation/chrome_site_per_process_browsertest.ccthird_party/blink/renderer/core/fileapi/url_file_api.cccontent/browser/renderer_host/render_frame_host_impl.ccthird_party/blink/renderer/core/workers/worklet_global_scope.ccthird_party/blink/renderer/core/workers/worklet_global_scope.hthird_party/blink/renderer/core/frame/local_frame.ccthird_party/blink/renderer/core/fileapi/public_url_manager.cc
Audit Directions
- Browser-side Mojo binders that skip process-type checksAudit other
Bind*Receivermethods onRenderFrameHostImpland related hosts that grant storage or origin-scoped capabilities, confirming each rejects PDF (is_pdf()) and other locked process types before binding. - Capabilities gated only in the rendererLook for features disabled solely via renderer-side predicates like
IsDomStorageDisabled()with no matching browser-side enforcement, since a compromised renderer bypasses renderer checks; verify the authoritative check lives in the browser. - Origin checks that misfit opaque/null originsWhere
CanAccessDataForOrigin()cannot be used because sandboxed/opaque-origin frames legitimately use null origins, confirm an alternative, correct discriminator (such asis_pdf()) is applied rather than the check being omitted.
Patch
From e062572426088d8c09a3e07cbb5d27f8cfd68508 Mon Sep 17 00:00:00 2001
From: Charlie Reis <creis@chromium.org>
Date: Wed, 16 Sep 2026 13:52:46 -0700
Subject: [PATCH] Reland: Prevent PDF processes from binding BlobURLStore
This reverts commit 7445311d483b3e6b3f572c84e7327f94438f0c12.
PDF renderer processes should have no need for creating or accessing
blob URLs. This CL prevents binding to BlobURLStore via the associated
(frame) and non-associated (worklet) interfaces, killing the renderer if
this is attempted.
Note that sandboxed frames (with opaque origins) are legitimately
allowed to create blob URLs with null origins, so
CanAccessDataForOrigin() cannot be used directly here.
The diff from PS1 addresses renderer kills that were encountered in the
wild after the fix initially landed. Non-compromised PDF renderer
processes now cleanly fail blob creation if it is attempted via features
like DevTools or extensions, rather than triggering a renderer kill.
There is also a kEnforcePdfBlobRestrictions killswitch in case further
regressions are encountered.
This CL was mostly written by Gemini.
Fixed: 540051167
Change-Id: Ib4ecdd2429999a58b9d7fe46d1704b24536eda15
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8396769
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Reviewed-by: Mingyu Lei <leimy@chromium.org>
Commit-Queue: Charlie Reis <creis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1699247}
---
diff --git a/content/browser/bad_message.h b/content/browser/bad_message.h
index 4ebb96b7..fc0c1c82 100644
--- a/content/browser/bad_message.h
+++ b/content/browser/bad_message.h
@@ -397,8 +397,8 @@
BIBI_BIND_VIBRATION_MANAGER_FOR_FENCED_FRAME = 369,
RFH_BEGIN_NAVIGATION_NO_INITIATOR_TOKENS = 370,
RFHI_INVALID_CLIENT_SIDE_REDIRECT_URL = 371,
- OBSOLETE_RFH_BLOB_URL_STORE_ASSOCIATED_PDF_PROCESS_BLOCKED = 372,
- OBSOLETE_RFH_BLOB_URL_STORE_RECEIVER_PDF_PROCESS_BLOCKED = 373,
+ RFH_BLOB_URL_STORE_ASSOCIATED_PDF_PROCESS_BLOCKED = 372,
+ RFH_BLOB_URL_STORE_RECEIVER_PDF_PROCESS_BLOCKED = 373,
RFH_DOWNLOAD_URL_INVALID_REFERRER = 374,
HSI_PLAY_HAPTICS_INVALID_INTENSITY = 375,
HSI_PLAY_HAPTICS_IN_FENCED_FRAME = 376,
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 53dd79f..61feedc 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -13806,6 +13806,18 @@
void RenderFrameHostImpl::BindBlobUrlStoreAssociatedReceiver(
mojo::PendingAssociatedReceiver<blink::mojom::BlobURLStore> receiver) {
CHECK_CURRENTLY_ON(BrowserThread::UI);
+ // Do not allow PDF renderers to access blob URLs, since they should never
+ // need them. Note that is_sandboxed() processes are legitimately allowed to
+ // create blob URLs with null origins, so CanAccessDataForOrigin() can't be
+ // used here. See also BindBlobUrlStoreReceiver.
+ if (base::FeatureList::IsEnabled(
+ blink::features::kEnforcePdfBlobRestrictions) &&
+ GetSiteInstance()->GetSiteInfo().is_pdf()) {
+ bad_message::ReceivedBadMessage(
+ GetProcess(),
+ bad_message::RFH_BLOB_URL_STORE_ASSOCIATED_PDF_PROCESS_BLOCKED);
+ return;
+ }
auto* storage_partition_impl =
static_cast<StoragePartitionImpl*>(GetStoragePartition());
@@ -13849,6 +13861,16 @@
void RenderFrameHostImpl::BindBlobUrlStoreReceiver(
mojo::PendingReceiver<blink::mojom::BlobURLStore> receiver) {
CHECK_CURRENTLY_ON(BrowserThread::UI);
+ // Do not bind blink.mojom.BlobURLStore for PDF renderers (see comment in
+ // BindBlobUrlStoreAssociatedReceiver).
+ if (base::FeatureList::IsEnabled(
+ blink::features::kEnforcePdfBlobRestrictions) &&
+ GetSiteInstance()->GetSiteInfo().is_pdf()) {
+ bad_message::ReceivedBadMessage(
+ GetProcess(),
+ bad_message::RFH_BLOB_URL_STORE_RECEIVER_PDF_PROCESS_BLOCKED);
+ return;
+ }
auto* storage_partition_impl =
static_cast<StoragePartitionImpl*>(GetStoragePartition());
diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc
index 02ab291..7bf84184 100644
--- a/third_party/blink/common/features.cc
+++ b/third_party/blink/common/features.cc
@@ -614,6 +614,10 @@
BASE_FEATURE(kEnforceNoopenerOnBlobURLNavigation,
base::FEATURE_ENABLED_BY_DEFAULT);
+// Killswitch for restrictions on blob URL creation in PDF processes, in case
+// regressions are encountered in the wild. See https://crbug.com/540051167.
+BASE_FEATURE(kEnforcePdfBlobRestrictions, base::FEATURE_ENABLED_BY_DEFAULT);
+
BASE_FEATURE(kEventTimingIgnorePresentationTimeFromUnexpectedFrameSource,
base::FEATURE_DISABLED_BY_DEFAULT);
diff --git a/third_party/blink/public/common/features.h b/third_party/blink/public/common/features.h
index 347447c..4f9ccbd2 100644
--- a/third_party/blink/public/common/features.h
+++ b/third_party/blink/public/common/features.h
@@ -421,6 +421,7 @@
kEnableDevtoolsDeepLinkViaExtensibilityApi);
BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kEnforceNoopenerOnBlobURLNavigation);
+BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kEnforcePdfBlobRestrictions);
BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(
kEventTimingIgnorePresentationTimeFromUnexpectedFrameSource);
diff --git a/third_party/blink/renderer/core/fileapi/public_url_manager.cc b/third_party/blink/renderer/core/fileapi/public_url_manager.cc
index 306d9bf..90884a0 100644
--- a/third_party/blink/renderer/core/fileapi/public_url_manager.cc
+++ b/third_party/blink/renderer/core/fileapi/public_url_manager.cc
@@ -30,11 +30,13 @@
#include "base/notreached.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
+#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/blob/blob.mojom-blink-forward.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fileapi/blob.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_client.h"
#include "third_party/blink/renderer/core/html/media/media_source_attachment.h"
#include "third_party/blink/renderer/core/html/media/media_source_registry.h"
#include "third_party/blink/renderer/core/workers/worker_global_scope.h"
@@ -64,7 +66,21 @@
worker_url_store_(execution_context) {
if (auto* window = DynamicTo<LocalDOMWindow>(execution_context)) {
LocalFrame* frame = window->GetFrame();
- if (!frame) {
+ // In PDF processes, DOM storage and Blob URLs are disabled to avoid
+ // accessing or registering data for the origin that served the PDF (see
+ // ChildProcessSecurityPolicyImpl::IsAccessAllowedForPdfProcess).
+ // Marking PublicURLManager as stopped ensures we do not bind
+ // frame_url_store_ (which would trigger a bad message renderer kill in the
+ // browser process) and causes URL.createObjectURL to return an empty
+ // string. An empty string was chosen rather than throwing a SecurityError
+ // to match the W3C File API specification when a blob URL cannot be
+ // generated, as well as the behavior of DOM storage in PDF processes (where
+ // localStorage returns null instead of throwing) and to avoid breaking
+ // extensions or scripts that do not expect createObjectURL to throw an
+ // exception.
+ if (!frame ||
+ (base::FeatureList::IsEnabled(features::kEnforcePdfBlobRestrictions) &&
+ frame->Client()->IsDomStorageDisabled())) {
is_stopped_ = true;
return;
}
@@ -93,7 +109,9 @@
if (worklet_global_scope->IsMainThreadWorkletGlobalScope()) {
LocalFrame* frame = worklet_global_scope->GetFrame();
- if (!frame) {
+ if (!frame || (base::FeatureList::IsEnabled(
+ features::kEnforcePdfBlobRestrictions) &&
+ frame->Client()->IsDomStorageDisabled())) {
is_stopped_ = true;
return;
}
@@ -107,7 +125,12 @@
// this worklet.
mojo::PendingRemote<mojom::blink::BlobURLStore> pending_remote =
worklet_global_scope->TakeBlobUrlStorePendingRemote();
- DCHECK(pending_remote.is_valid());
+ // The BlobURLStore remote may be invalid if blobs are not allowed in
+ // this process (e.g., in PDF processes where DOM storage is disabled).
+ if (!pending_remote.is_valid()) {
+ is_stopped_ = true;
+ return;
+ }
worker_url_store_.Bind(
std::move(pending_remote),
execution_context->GetTaskRunner(TaskType::kFileReading));
diff --git a/third_party/blink/renderer/core/fileapi/url_file_api.cc b/third_party/blink/renderer/core/fileapi/url_file_api.cc
index 1463601..daa3986e 100644
--- a/third_party/blink/renderer/core/fileapi/url_file_api.cc
+++ b/third_party/blink/renderer/core/fileapi/url_file_api.cc
@@ -24,6 +24,14 @@
DCHECK(execution_context);
UseCounter::Count(execution_context, WebFeature::kCreateObjectURLBlob);
+ // Note: If blob URL creation is disabled in this context (such as in PDF
+ // processes where LocalFrameClient::IsDomStorageDisabled() is true),
+ // PublicURLManager is stopped and URL::CreatePublicURL() returns an empty
+ // string. An empty string was chosen rather than throwing a SecurityError to
+ // match the W3C File API specification when a blob URL cannot be generated,
+ // as well as the behavior of DOM storage in PDF processes (where localStorage
+ // returns null instead of throwing) and to avoid breaking extensions or
+ // scripts that do not expect createObjectURL to throw an exception.
return URL::CreatePublicURL(execution_context, blob);
}
diff --git a/third_party/blink/renderer/core/frame/local_frame.cc b/third_party/blink/renderer/core/frame/local_frame.cc
index bea3d55..d4ab4f8 100644
--- a/third_party/blink/renderer/core/frame/local_frame.cc
+++ b/third_party/blink/renderer/core/frame/local_frame.cc
@@ -4343,6 +4343,10 @@
mojo::PendingRemote<mojom::blink::BlobURLStore>
LocalFrame::GetBlobUrlStorePendingRemote() {
+ if (base::FeatureList::IsEnabled(features::kEnforcePdfBlobRestrictions) &&
+ Client()->IsDomStorageDisabled()) {
+ return mojo::NullRemote();
+ }
mojo::PendingRemote<mojom::blink::BlobURLStore> pending_remote;
GetBrowserInterfaceBroker().GetInterface(
pending_remote.InitWithNewPipeAndPassReceiver());
diff --git a/third_party/blink/renderer/core/workers/worklet_global_scope.cc b/third_party/blink/renderer/core/workers/worklet_global_scope.cc
index 9e4968b3..c1cd4d7 100644
--- a/third_party/blink/renderer/core/workers/worklet_global_scope.cc
+++ b/third_party/blink/renderer/core/workers/worklet_global_scope.cc
@@ -368,7 +368,6 @@
mojo::PendingRemote<mojom::blink::BlobURLStore>
WorkletGlobalScope::TakeBlobUrlStorePendingRemote() {
- DCHECK(blob_url_store_pending_remote_.is_valid());
return std::move(blob_url_store_pending_remote_);
}
diff --git a/third_party/blink/renderer/core/workers/worklet_global_scope.h b/third_party/blink/renderer/core/workers/worklet_global_scope.h
index 285326d9..0b71a84 100644
--- a/third_party/blink/renderer/core/workers/worklet_global_scope.h
+++ b/third_party/blink/renderer/core/workers/worklet_global_scope.h
@@ -84,7 +84,9 @@
// Returns `blob_url_store_pending_remote_` for use when instantiating the
// PublicURLManager in threaded worklet contexts. This method should only be
- // called once. See `blob_url_store_pending_remote_` for more details.
+ // called once. The returned remote may be invalid if blob storage is
+ // disabled for the creating frame (e.g., in PDF processes). See
+ // `blob_url_store_pending_remote_` for more details.
mojo::PendingRemote<mojom::blink::BlobURLStore>
TakeBlobUrlStorePendingRemote();
@@ -222,6 +224,8 @@
// parent frame's BrowserInterfaceBroker and used when instantiating the
// worklet's PublicURLManager. This remote is used for Blob URL related
// functionality such as registering, revoking, and navigating to Blob URLs.
+ // It may be invalid if blob storage is disabled for the creating frame
+ // (e.g., in PDF processes).
mojo::PendingRemote<mojom::blink::BlobURLStore>
blob_url_store_pending_remote_;
};
diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml
index 6b50f81e..1a63ec7 100644
--- a/tools/metrics/histograms/metadata/stability/enums.xml
+++ b/tools/metrics/histograms/metadata/stability/enums.xml
@@ -551,10 +551,8 @@
<int value="369" label="BIBI_BIND_VIBRATION_MANAGER_FOR_FENCED_FRAME"/>
<int value="370" label="RFH_BEGIN_NAVIGATION_NO_INITIATOR_TOKENS"/>
<int value="371" label="RFHI_INVALID_CLIENT_SIDE_REDIRECT_URL"/>
- <int value="372"
- label="OBSOLETE_RFH_BLOB_URL_STORE_ASSOCIATED_PDF_PROCESS_BLOCKED"/>
- <int value="373"
- label="OBSOLETE_RFH_BLOB_URL_STORE_RECEIVER_PDF_PROCESS_BLOCKED"/>
+ <int value="372" label="RFH_BLOB_URL_STORE_ASSOCIATED_PDF_PROCESS_BLOCKED"/>
+ <int value="373" label="RFH_BLOB_URL_STORE_RECEIVER_PDF_PROCESS_BLOCKED"/>
<int value="374" label="RFH_DOWNLOAD_URL_INVALID_REFERRER"/>
<int value="375" label="HSI_PLAY_HAPTICS_INVALID_INTENSITY"/>
<int value="376" label="HSI_PLAY_HAPTICS_IN_FENCED_FRAME"/>
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 cc8b0b2..0e3555a 100644
--- a/chrome/browser/site_isolation/chrome_site_per_process_browsertest.cc
+++ b/chrome/browser/site_isolation/chrome_site_per_process_browsertest.cc
@@ -499,6 +499,76 @@
"})()"));
EXPECT_TRUE(pdf_frame->IsRenderFrameLive());
}
+
+// Check that navigating to a PDF and then trying to create a blob URL in the
+// context of the PDF document fails gracefully and doesn't lead to a renderer
+// kill. PDF documents don't create blob URLs directly, but the access could
+// still happen via DevTools. See https://crbug.com/540051167 and
+// https://crbug.com/550946804.
+IN_PROC_BROWSER_TEST_F(ChromeSitePerProcessOopifPDFTest,
+ CreateBlobUrlInPDFDocument) {
+ 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 blob URL in the PDF document should return an empty
+ // string (rather than throwing a SecurityError, matching the File API spec
+ // and DOM storage behavior) and should not kill the renderer.
+ EXPECT_EQ(
+ "", content::EvalJs(pdf_frame, "URL.createObjectURL(new Blob(['foo']))"));
+ EXPECT_TRUE(pdf_frame->IsRenderFrameLive());
+}
+
+// Check that navigating to a PDF and then trying to create an AudioWorklet in
+// the context of the PDF document fails gracefully and doesn't lead to a
+// renderer kill. Creating an AudioWorklet causes the renderer to obtain a
+// BlobURLStore remote for the worklet, but in PDF processes
+// LocalFrame::GetBlobUrlStorePendingRemote returns an invalid remote to avoid
+// requesting the interface from the browser. See https://crbug.com/540051167.
+IN_PROC_BROWSER_TEST_F(ChromeSitePerProcessOopifPDFTest,
+ CreateAudioWorkletInPDFDocument) {
+ 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());
+
+ // Creating an AudioWorklet causes the renderer to get a BlobURLStore for the
+ // worklet. In PDF processes, LocalFrame::GetBlobUrlStorePendingRemote avoids
+ // requesting this interface from the browser, so the renderer should not be
+ // killed.
+ EXPECT_TRUE(
+ ExecJs(pdf_frame,
+ "const context = new OfflineAudioContext(1, 1, 44100);"
+ "context.audioWorklet.addModule('data:text/javascript,');"));
+ 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/blob_storage/blob_url_browsertest.cc b/content/browser/blob_storage/blob_url_browsertest.cc
index 9cca5d3..075de964 100644
--- a/content/browser/blob_storage/blob_url_browsertest.cc
+++ b/content/browser/blob_storage/blob_url_browsertest.cc
@@ -12,12 +12,14 @@
#include "base/test/values_test_util.h"
#include "base/test/with_feature_override.h"
#include "build/build_config.h"
+#include "content/browser/bad_message.h"
#include "content/browser/devtools/render_frame_devtools_agent_host.h"
#include "content/browser/permissions/permission_controller_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_context.h"
+#include "content/public/browser/navigation_controller.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
@@ -870,4 +872,118 @@
EXPECT_FALSE(handle_null);
}
+// Verifies that PDF processes cannot bind to BlobURLStore, using the associated
+// interface that is used by frames. See https://crbug.com/540051167.
+IN_PROC_BROWSER_TEST_F(BlobUrlBrowserTest, BlobUrlBlockedForPdfProcess) {
+ WebContentsImpl* tab = static_cast<WebContentsImpl*>(shell()->web_contents());
+ GURL url = embedded_test_server()->GetURL("a.test", "/empty.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());
+
+ // The SiteInstance and RenderProcessHost are treated as PDFs. However, the
+ // renderer process will not realize that blob creation should fail gracefully
+ // because ContentBrowserClient::IsDomStorageDisabled() defaults to false, and
+ // the PDF check in the renderer is implemented in the chrome/ layer. This
+ // happens to make testing the renderer kill easier below.
+ RenderFrameHostImpl* frame =
+ static_cast<RenderFrameHostImpl*>(tab->GetPrimaryMainFrame());
+ ASSERT_TRUE(frame->GetSiteInstance()->GetSiteInfo().is_pdf());
+ ASSERT_TRUE(frame->GetProcess()->IsPdf());
+
... (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 cc8b0b2..0e3555a 100644
--- a/chrome/browser/site_isolation/chrome_site_per_process_browsertest.cc
+++ b/chrome/browser/site_isolation/chrome_site_per_process_browsertest.cc
@@ -499,6 +499,76 @@
"})()"));
EXPECT_TRUE(pdf_frame->IsRenderFrameLive());
}
+
+// Check that navigating to a PDF and then trying to create a blob URL in the
+// context of the PDF document fails gracefully and doesn't lead to a renderer
+// kill. PDF documents don't create blob URLs directly, but the access could
+// still happen via DevTools. See https://crbug.com/540051167 and
+// https://crbug.com/550946804.
+IN_PROC_BROWSER_TEST_F(ChromeSitePerProcessOopifPDFTest,
+ CreateBlobUrlInPDFDocument) {
+ 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 blob URL in the PDF document should return an empty
+ // string (rather than throwing a SecurityError, matching the File API spec
+ // and DOM storage behavior) and should not kill the renderer.
+ EXPECT_EQ(
+ "", content::EvalJs(pdf_frame, "URL.createObjectURL(new Blob(['foo']))"));
+ EXPECT_TRUE(pdf_frame->IsRenderFrameLive());
+}
+
+// Check that navigating to a PDF and then trying to create an AudioWorklet in
+// the context of the PDF document fails gracefully and doesn't lead to a
+// renderer kill. Creating an AudioWorklet causes the renderer to obtain a
+// BlobURLStore remote for the worklet, but in PDF processes
+// LocalFrame::GetBlobUrlStorePendingRemote returns an invalid remote to avoid
+// requesting the interface from the browser. See https://crbug.com/540051167.
+IN_PROC_BROWSER_TEST_F(ChromeSitePerProcessOopifPDFTest,
+ CreateAudioWorkletInPDFDocument) {
+ 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());
+
+ // Creating an AudioWorklet causes the renderer to get a BlobURLStore for the
+ // worklet. In PDF processes, LocalFrame::GetBlobUrlStorePendingRemote avoids
+ // requesting this interface from the browser, so the renderer should not be
+ // killed.
+ EXPECT_TRUE(
+ ExecJs(pdf_frame,
+ "const context = new OfflineAudioContext(1, 1, 44100);"
+ "context.audioWorklet.addModule('data:text/javascript,');"));
+ 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/blob_storage/blob_url_browsertest.cc b/content/browser/blob_storage/blob_url_browsertest.cc
index 9cca5d3..075de964 100644
--- a/content/browser/blob_storage/blob_url_browsertest.cc
+++ b/content/browser/blob_storage/blob_url_browsertest.cc
@@ -12,12 +12,14 @@
#include "base/test/values_test_util.h"
#include "base/test/with_feature_override.h"
#include "build/build_config.h"
+#include "content/browser/bad_message.h"
#include "content/browser/devtools/render_frame_devtools_agent_host.h"
#include "content/browser/permissions/permission_controller_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_context.h"
+#include "content/public/browser/navigation_controller.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
@@ -870,4 +872,118 @@
EXPECT_FALSE(handle_null);
}
+// Verifies that PDF processes cannot bind to BlobURLStore, using the associated
+// interface that is used by frames. See https://crbug.com/540051167.
+IN_PROC_BROWSER_TEST_F(BlobUrlBrowserTest, BlobUrlBlockedForPdfProcess) {
+ WebContentsImpl* tab = static_cast<WebContentsImpl*>(shell()->web_contents());
+ GURL url = embedded_test_server()->GetURL("a.test", "/empty.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());
+
+ // The SiteInstance and RenderProcessHost are treated as PDFs. However, the
+ // renderer process will not realize that blob creation should fail gracefully
+ // because ContentBrowserClient::IsDomStorageDisabled() defaults to false, and
+ // the PDF check in the renderer is implemented in the chrome/ layer. This
+ // happens to make testing the renderer kill easier below.
+ RenderFrameHostImpl* frame =
+ static_cast<RenderFrameHostImpl*>(tab->GetPrimaryMainFrame());
+ ASSERT_TRUE(frame->GetSiteInstance()->GetSiteInfo().is_pdf());
+ ASSERT_TRUE(frame->GetProcess()->IsPdf());
+
+ // Listen for the renderer kill and bad message reason.
+ RenderProcessHostBadIpcMessageWaiter kill_waiter(frame->GetProcess());
+
+ // Executing `URL.createObjectURL` triggers PublicURLManager in the renderer
+ // to bind blink.mojom.BlobURLStore via the associated interface on
+ // RenderFrameHost. The browser must reject this and terminate the process.
+ std::ignore = ExecJs(frame, "URL.createObjectURL(new Blob(['payload']))");
+
+ EXPECT_EQ(bad_message::RFH_BLOB_URL_STORE_ASSOCIATED_PDF_PROCESS_BLOCKED,
+ kill_waiter.Wait());
+}
+
+// Verifies that PDF processes cannot bind to BlobURLStore, using the interface
+// broker that is used by threaded worklets. See https://crbug.com/540051167.
+IN_PROC_BROWSER_TEST_F(BlobUrlBrowserTest,
+ BlobUrlWorkletReceiverBlockedForPdfProcess) {
+ WebContentsImpl* tab = static_cast<WebContentsImpl*>(shell()->web_contents());
+ // AudioWorklet requires a secure context (e.g., localhost).
+ GURL url = embedded_test_server()->GetURL("localhost", "/empty.html");
+
+ 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());
+
+ // The SiteInstance and RenderProcessHost are treated as PDFs. However, the
+ // renderer process will not realize that blob creation should fail gracefully
+ // because ContentBrowserClient::IsDomStorageDisabled() defaults to false, and
+ // the PDF check in the renderer is implemented in the chrome/ layer. This
+ // happens to make testing the renderer kill easier below.
+ RenderFrameHostImpl* frame =
+ static_cast<RenderFrameHostImpl*>(tab->GetPrimaryMainFrame());
+ ASSERT_TRUE(frame->GetSiteInstance()->GetSiteInfo().is_pdf());
+ ASSERT_TRUE(frame->GetProcess()->IsPdf());
+
+ RenderProcessHostBadIpcMessageWaiter kill_waiter(frame->GetProcess());
+
+ // Creating an AudioWorklet triggers the renderer to request
+ // blink.mojom.BlobURLStore for the worklet via the BrowserInterfaceBroker.
+ std::ignore =
+ ExecJs(frame,
+ "const context = new OfflineAudioContext(1, 1, 44100);"
+ "context.audioWorklet.addModule('data:text/javascript,');");
+
+ EXPECT_EQ(bad_message::RFH_BLOB_URL_STORE_RECEIVER_PDF_PROCESS_BLOCKED,
+ kill_waiter.Wait());
+}
+
+class BlobUrlPdfKillswitchDisabledBrowserTest : public BlobUrlBrowserTest {
+ public:
+ BlobUrlPdfKillswitchDisabledBrowserTest() {
+ feature_list_.InitAndDisableFeature(
+ blink::features::kEnforcePdfBlobRestrictions);
+ }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(BlobUrlPdfKillswitchDisabledBrowserTest,
+ BlobUrlAllowedForPdfProcess) {
+ WebContentsImpl* tab = static_cast<WebContentsImpl*>(shell()->web_contents());
+ GURL url = embedded_test_server()->GetURL("a.test", "/empty.html");
+
+ 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 =
+ static_cast<RenderFrameHostImpl*>(tab->GetPrimaryMainFrame());
+ ASSERT_TRUE(frame->GetSiteInstance()->GetSiteInfo().is_pdf());
+
+ // Executing `URL.createObjectURL` should not kill the renderer when the
+ // EnforcePdfBlobRestrictions killswitch is disabled.
+ EXPECT_TRUE(ExecJs(frame, "URL.createObjectURL(new Blob(['payload']))"));
+ EXPECT_TRUE(frame->IsRenderFrameLive());
+}
+
} // namespace content
diff --git a/third_party/blink/renderer/core/fileapi/public_url_manager_test.cc b/third_party/blink/renderer/core/fileapi/public_url_manager_test.cc
index 9860d61..6c984087 100644
--- a/third_party/blink/renderer/core/fileapi/public_url_manager_test.cc
+++ b/third_party/blink/renderer/core/fileapi/public_url_manager_test.cc
@@ -8,10 +8,12 @@
#include <utility>
#include "base/memory/scoped_refptr.h"
+#include "base/test/scoped_feature_list.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/platform/web_media_source.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
@@ -20,6 +22,7 @@
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/html/media/media_source_attachment.h"
#include "third_party/blink/renderer/core/html/media/media_source_registry.h"
+#include "third_party/blink/renderer/core/loader/empty_clients.h"
#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
#include "third_party/blink/renderer/platform/blob/testing/fake_blob.h"
#include "third_party/blink/renderer/platform/blob/testing/fake_blob_url_store.h"
@@ -252,4 +255,56 @@
EXPECT_TRUE(url_store_.revocations.empty());
}
+namespace {
+
+class TestPdfLocalFrameClient : public EmptyLocalFrameClient {
+ public:
+ bool IsDomStorageDisabled() const override { return true; }
+};
+
+} // namespace
+
+TEST_F(PublicURLManagerTest, DisabledWhenDomStorageDisabled) {
+ // In PDF processes (or other contexts where IsDomStorageDisabled() is true),
+ // PublicURLManager should be stopped by default, returning an empty string
+ // instead of binding the BlobURLStore.
+ {
+ auto page_holder = std::make_unique<DummyPageHolder>(
+ gfx::Size(), /*chrome_client=*/nullptr,
+ MakeGarbageCollected<TestPdfLocalFrameClient>());
+ auto* window = page_holder->GetFrame().DomWindow();
+ PublicURLManager& manager = window->GetPublicURLManager();
+
+ Blob* blob = MakeGarbageCollected<Blob>(
+ BlobDataHandle::Create("id", "", 0, CreateMojoBlob("id")));
+ EXPECT_TRUE(manager.RegisterUrl(blob).empty());
+ }
+
+ // When the EnforcePdfBlobRestrictions killswitch is disabled,
+ // PublicURLManager should not be stopped even if IsDomStorageDisabled() is
+ // true.
+ {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndDisableFeature(
+ features::kEnforcePdfBlobRestrictions);
+
+ auto page_holder = std::make_unique<DummyPageHolder>(
+ gfx::Size(), /*chrome_client=*/nullptr,
+ MakeGarbageCollected<TestPdfLocalFrameClient>());
+ auto* window = page_holder->GetFrame().DomWindow();
+ PublicURLManager& manager = window->GetPublicURLManager();
+
+ HeapMojoAssociatedRemote<mojom::blink::BlobURLStore> url_store_remote(
+ window);
+ FakeBlobURLStore url_store;
+ mojo::AssociatedReceiver<mojom::blink::BlobURLStore> url_store_receiver(
+ &url_store, url_store_remote.BindNewEndpointAndPassDedicatedReceiver());
+ manager.SetURLStoreForTesting(std::move(url_store_remote));
+
+ Blob* blob = MakeGarbageCollected<Blob>(
+ BlobDataHandle::Create("id", "", 0, CreateMojoBlob("id")));
+ EXPECT_FALSE(manager.RegisterUrl(blob).empty());
+ }
+}
+
... (truncated)