CVE-2026-78953
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
IN_PROC_BROWSER_TEST_Fcontent/browser/renderer_host/cookie_browsertest.cc |
modified |
Files Changed
content/browser/renderer_host/cookie_browsertest.cccontent/browser/renderer_host/render_frame_host_impl.cc
Patch
From 03b18568967f29eb7d198a513c9be98291a5ce6c Mon Sep 17 00:00:00 2001 From: Alex Moshchuk <[email protected]> Date: Wed, 22 Jul 2026 13:23:28 -0700 Subject: [PATCH] Block PDF processes from binding RestrictedCookieManager RenderFrameHostImpl::SendCommitNavigation() already skips the eager RestrictedCookieManager bind [1] for PDF processes, since PDF renderers are isolated from cookies and storage for the origin that served the PDF. The BrowserInterfaceBroker fallback path, RenderFrameHostImpl::BindRestrictedCookieManager(), did not have the same gate, so a frame committed in a PDF-isolated process could still obtain a RestrictedCookieManager for its committed origin via the broker. Add a CanAccessDataForOrigin check to BindRestrictedCookieManager() so that we do not grant the cookie manager interface to PDF renderer processes, and also to sandboxed frame processes as a bonus. There's intentionally no renderer kill added, because the renderer doesn't prevent a sandboxed frame process from requesting this interface via cookieStore.get(). (The renderer does have subsequent security checks [2] to deny access to cookies from opaque origins before it ever tries to use that interface.) Add a (mostly Gemini-written) content_browsertest that commits a page with LoadURLParams::is_pdf set and verifies that document.cookie can neither read nor write cookies for the committed origin. The new test fails without the fix applied. [1] https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/render_frame_host_impl.cc;l=17120;drc=b251030f22d6810f6755262d2182316f60552d60 [2] https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/cookie_store/cookie_store.cc;l=506;drc=bd8f7f70a1197b77f4bb0a886e2778f707e333e1 Bug: 516665605 Change-Id: Id5f13ce3f10cec726c5736daffcadcfbdf67001f Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8127316 Reviewed-by: Charlie Reis <[email protected]> Commit-Queue: Alex Moshchuk <[email protected]> Cr-Commit-Position: refs/heads/main@{#1666575} --- diff --git a/content/browser/renderer_host/cookie_browsertest.cc b/content/browser/renderer_host/cookie_browsertest.cc index 015c8a1d..708a2836 100644 --- a/content/browser/renderer_host/cookie_browsertest.cc +++ b/content/browser/renderer_host/cookie_browsertest.cc @@ -22,6 +22,7 @@ #include "content/public/browser/browser_context.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" +#include "content/public/browser/navigation_controller.h" #include "content/public/browser/render_process_host.h" #include "content/public/browser/security_principal.h" #include "content/public/browser/site_isolation_policy.h" @@ -832,6 +833,40 @@ v.DepictFrameTree(tab->GetPrimaryFrameTree().root())); } +// Verifies that a frame committed in a PDF-isolated process cannot bind a +// RestrictedCookieManager for the committed origin. The SendCommitNavigation +// path already skips the bind for PDF processes, so this exercises the +// BrowserInterfaceBroker fallback used when no cookie manager was supplied at +// commit time. +IN_PROC_BROWSER_TEST_F(CookieBrowserTest, CookiesBlockedForPdfProcess) { + ASSERT_TRUE(embedded_test_server()->Start()); + + WebContentsImpl* tab = static_cast<WebContentsImpl*>(shell()->web_contents()); + GURL url = embedded_test_server()->GetURL("a.test", "/empty.html"); + + SetCookieDirect(tab, url, "A=1"); + ASSERT_EQ("A=1", GetCookiesDirect(tab, url)); + + // 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()); + + RenderFrameHost* frame = tab->GetPrimaryMainFrame(); + + // The PDF process must not be able to read or write cookies for the committed + // origin. + EXPECT_EQ("", GetCookieFromJS(frame)); + std::ignore = EvalJs(frame, "document.cookie = 'B=2'"); + EXPECT_EQ("A=1", GetCookiesDirect(tab, url)); +} + IN_PROC_BROWSER_TEST_F(CookieBrowserTest, CookieNotReadableAfterExpiry) { ASSERT_TRUE(embedded_test_server()->Start()); diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc index f3e0c1f..ca28b2c3 100644 --- a/content/browser/renderer_host/render_frame_host_impl.cc +++ b/content/browser/renderer_host/render_frame_host_impl.cc @@ -15058,6 +15058,24 @@ void RenderFrameHostImpl::BindRestrictedCookieManager( mojo::PendingReceiver<network::mojom::RestrictedCookieManager> receiver) { + // Check whether the current frame is permitted to access cookies. For + // example, this will avoid binding the interface for PDF or sandboxed frame + // processes that should never need to access cookies. + auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); + if (!policy->CanAccessDataForOrigin(GetProcess()->GetID().GetUnsafeValue(), + GetLastCommittedOrigin())) { + // Note that there's intentionally no renderer kill here because the + // renderer doesn't prevent code in a sandboxed frame from requesting this + // interface via cookieStore.get() (despite such a frame having an opaque + // origin and no access to any cookies). The renderer does have subsequent + // security checks to deny access to cookies from opaque origins before it + // ever tries to use that interface, though. If a compromised renderer skips + // those checks and attempts to send IPCs to read/write cookies, those IPCs + // would fail if this interface isn't bound; writes would therefore be + // no-ops and reads would behave as if there are no cookies. + return; + } + BindRestrictedCookieManagerWithOrigin( std::move(receiver), GetIsolationInfoForSubresources(), GetLastCommittedOrigin(), GetCookieSettingOverrides());
Regression Test / PoC
diff --git a/content/browser/renderer_host/cookie_browsertest.cc b/content/browser/renderer_host/cookie_browsertest.cc
index 015c8a1d..708a2836 100644
--- a/content/browser/renderer_host/cookie_browsertest.cc
+++ b/content/browser/renderer_host/cookie_browsertest.cc
@@ -22,6 +22,7 @@
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/security_principal.h"
#include "content/public/browser/site_isolation_policy.h"
@@ -832,6 +833,40 @@
v.DepictFrameTree(tab->GetPrimaryFrameTree().root()));
}
+// Verifies that a frame committed in a PDF-isolated process cannot bind a
+// RestrictedCookieManager for the committed origin. The SendCommitNavigation
+// path already skips the bind for PDF processes, so this exercises the
+// BrowserInterfaceBroker fallback used when no cookie manager was supplied at
+// commit time.
+IN_PROC_BROWSER_TEST_F(CookieBrowserTest, CookiesBlockedForPdfProcess) {
+ ASSERT_TRUE(embedded_test_server()->Start());
+
+ WebContentsImpl* tab = static_cast<WebContentsImpl*>(shell()->web_contents());
+ GURL url = embedded_test_server()->GetURL("a.test", "/empty.html");
+
+ SetCookieDirect(tab, url, "A=1");
+ ASSERT_EQ("A=1", GetCookiesDirect(tab, url));
+
+ // 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());
+
+ RenderFrameHost* frame = tab->GetPrimaryMainFrame();
+
+ // The PDF process must not be able to read or write cookies for the committed
+ // origin.
+ EXPECT_EQ("", GetCookieFromJS(frame));
+ std::ignore = EvalJs(frame, "document.cookie = 'B=2'");
+ EXPECT_EQ("A=1", GetCookiesDirect(tab, url));
+}
+
IN_PROC_BROWSER_TEST_F(CookieBrowserTest, CookieNotReadableAfterExpiry) {
ASSERT_TRUE(embedded_test_server()->Start());
Original Bug Report
PDF process isolation bypass via lazy RestrictedCookieManager binding
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: A compromised PDF renderer process can bypass storage access controls by lazy-binding a RestrictedCookieManager interface. Although cookie/storage access is blocked for PDF processes on the eager navigation commit path, the lazy path via BrowserInterfaceBroker lacks any PDF-specific verification. This allows a compromised PDF renderer to retrieve and modify non-HttpOnly cookies for the hosting origin.
Affected files:
content/browser/renderer_host/render_frame_host_impl.cccontent/browser/browser_interface_binders.cc
Estimated timestamp from git blame: 2024-06-26
Root Cause Analysis
In Chromium, PDF documents are isolated into specialized, sandboxed renderer processes. These processes are intended to operate under a strict storage access block. ChildProcessSecurityPolicyImpl::IsAccessAllowedForPdfProcess explicitly returns false for AccessType::kCanAccessDataForCommittedOrigin to prevent PDF renderers from accessing passwords, storage, or cookies for the hosting origin.
To enforce this, the eager navigation commit-time path in RenderFrameHostImpl::SendCommitNavigation checks should_block_storage_access_for_pdf (based on GetSiteInstance()->GetSiteInfo().is_pdf()) and skips binding a RestrictedCookieManager:
// content/browser/renderer_host/render_frame_host_impl.cc
bool should_block_storage_access_for_pdf =
GetSiteInstance()->GetSiteInfo().is_pdf();
...
if (common_params->url.SchemeIsHTTPOrHTTPS() && !origin_to_commit.opaque() &&
!should_block_storage_access_for_pdf && ...) {
cookie_manager_info = mojom::CookieManagerInfo::New();
...
BindRestrictedCookieManagerWithOrigin(...); // <-- Skipped for PDF
}
However, the lazy binding path—used when a renderer explicitly requests the interface via the BrowserInterfaceBroker—completely lacks any equivalent check.
In content/browser/browser_interface_binders.cc, the broker is registered without any PDF-specific constraints:
map->Add<network::mojom::RestrictedCookieManager>(
&BindRenderFrameHostImpl<
&RenderFrameHostImpl::BindRestrictedCookieManager>);
When invoked, this routes to RenderFrameHostImpl::BindRestrictedCookieManager, which binds the manager using the frame’s last committed origin without verifying whether the process is a PDF process, and without querying ChildProcessSecurityPolicy:
void RenderFrameHostImpl::BindRestrictedCookieManager(
mojo::PendingReceiver<network::mojom::RestrictedCookieManager> receiver) {
BindRestrictedCookieManagerWithOrigin(
std::move(receiver), GetIsolationInfoForSubresources(),
GetLastCommittedOrigin(), GetCookieSettingOverrides());
}
Because the subsequent layers in StoragePartitionImpl and NetworkContext assume that the browser process has already enforced access control before requesting the RestrictedCookieManager from the out-of-process Network Service, the interface is successfully bound and returned to the renderer.
Potential Attack Scenario
(Note: These are suggested/potential steps; our tooling does not have the ability to run code or verify a live exploit)
- An attacker uploads a malicious PDF file onto a target origin (e.g.,
https://victim.example/malicious.pdf). - A user navigates to the PDF. The browser spawns an isolated PDF renderer process for it.
- The attacker exploits a memory corruption vulnerability (such as a PDFium bug) in the sandboxed PDF renderer process to achieve arbitrary code execution within the renderer.
- The compromised PDF renderer requests the ’network.mojom.RestrictedCookieManager’ interface via its
BrowserInterfaceBrokerpipe. - The browser process maps the request to
RenderFrameHostImpl::BindRestrictedCookieManagerand binds it using the frame’s last committed origin (https://victim.example) without any PDF validation. - The compromised PDF renderer uses the resulting
RestrictedCookieManagerinterface to read and write non-HttpOnly cookies forhttps://victim.example, bypassing the PDF isolation storage restrictions.
Suggested Fix
Add a check in RenderFrameHostImpl::BindRestrictedCookieManager to prevent PDF frames from lazy-binding the interface. If GetSiteInstance()->GetSiteInfo().is_pdf() is true, the request should be blocked (and ideally, a bad message should be reported to terminate the compromised renderer):
void RenderFrameHostImpl::BindRestrictedCookieManager(
mojo::PendingReceiver<network::mojom::RestrictedCookieManager> receiver) {
if (GetSiteInstance()->GetSiteInfo().is_pdf()) {
mojo::ReportBadMessage('PDF processes are not allowed to access cookies.');
return;
}
BindRestrictedCookieManagerWithOrigin(
std::move(receiver), GetIsolationInfoForSubresources(),
GetLastCommittedOrigin(), GetCookieSettingOverrides());
}
Evaluated with Chrome root at commit: a2bea94528f4bd6cc57739c43fa3bb890b8367d3
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.