CVE-2026-113153
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
IN_PROC_BROWSER_TEST_Pcontent/browser/storage_access/storage_access_browsertest.cc |
modified |
Files Changed
content/browser/storage_access/storage_access_browsertest.cccontent/browser/renderer_host/render_frame_host_impl.cc
Patch
From 7a02fbd106378d4d3b66ca6f7a980003f00aa1be Mon Sep 17 00:00:00 2001 From: Chris Fredrickson <[email protected]> Date: Thu, 17 Sep 2026 15:12:18 -0700 Subject: [PATCH] [SAA] Check CanAccessDataForOrigin in IsStorageAccessRestricted This means that all callers (which includes permission requests, StorageAccessHandle binding, DOM storage binding, and permission renewals) will verify that the frame is not prevented from accessing that data by ChildProcessSecurityPolicyImpl. Fixed: 561099700 Change-Id: Icd70880c2fdb965941d7f4dfdbbe8e4ae30e1386 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8420925 Commit-Queue: Alex Moshchuk <[email protected]> Auto-Submit: Chris Fredrickson <[email protected]> Reviewed-by: Alex Moshchuk <[email protected]> Cr-Commit-Position: refs/heads/main@{#1700162} --- diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc index d22caf6..741c26f 100644 --- a/content/browser/renderer_host/render_frame_host_impl.cc +++ b/content/browser/renderer_host/render_frame_host_impl.cc @@ -13805,7 +13805,9 @@ IsNestedWithinFencedFrame() || IsSandboxed( network::mojom::WebSandboxFlags::kStorageAccessByUserActivation) || - GetStorageKey().ForbidsUnpartitionedStorageAccess(); + GetStorageKey().ForbidsUnpartitionedStorageAccess() || + !ChildProcessSecurityPolicyImpl::GetInstance()->CanAccessDataForOrigin( + GetProcess()->GetID().GetUnsafeValue(), GetLastCommittedOrigin()); } void RenderFrameHostImpl::BindBlobUrlStoreAssociatedReceiver( diff --git a/content/browser/storage_access/storage_access_browsertest.cc b/content/browser/storage_access/storage_access_browsertest.cc index c91ad2d..7e60005 100644 --- a/content/browser/storage_access/storage_access_browsertest.cc +++ b/content/browser/storage_access/storage_access_browsertest.cc @@ -7,6 +7,7 @@ #include "content/browser/permissions/permission_controller_impl.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/public/test/browser_test.h" +#include "content/public/test/browser_test_utils.h" #include "content/public/test/content_browser_test.h" #include "content/public/test/content_browser_test_content_browser_client.h" #include "content/public/test/content_browser_test_utils.h" @@ -260,6 +261,26 @@ expected_restricted_handle_result()); } +IN_PROC_BROWSER_TEST_P(StorageAccessBrowserTest, + BindStorageAccessHandle_PdfRenderer) { + WebContentsImpl* tab = static_cast<WebContentsImpl*>(shell()->web_contents()); + GURL url = embedded_https_test_server().GetURL("a.test", "/simple_page.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()); + ASSERT_TRUE(host()->GetSiteInstance()->GetSiteInfo().is_pdf()); + ASSERT_TRUE(host()->GetProcess()->IsPdf()); + + EXPECT_EQ(BindStorageAccessHandleInFrame(host()), + expected_restricted_handle_result()); +} + IN_PROC_BROWSER_TEST_P(StorageAccessBrowserTest, BindDomStorage) { EXPECT_EQ(BindDomStorage(), is_cookie_access_allowed()); }
Regression Test / PoC
diff --git a/content/browser/storage_access/storage_access_browsertest.cc b/content/browser/storage_access/storage_access_browsertest.cc
index c91ad2d..7e60005 100644
--- a/content/browser/storage_access/storage_access_browsertest.cc
+++ b/content/browser/storage_access/storage_access_browsertest.cc
@@ -7,6 +7,7 @@
#include "content/browser/permissions/permission_controller_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
@@ -260,6 +261,26 @@
expected_restricted_handle_result());
}
+IN_PROC_BROWSER_TEST_P(StorageAccessBrowserTest,
+ BindStorageAccessHandle_PdfRenderer) {
+ WebContentsImpl* tab = static_cast<WebContentsImpl*>(shell()->web_contents());
+ GURL url = embedded_https_test_server().GetURL("a.test", "/simple_page.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());
+ ASSERT_TRUE(host()->GetSiteInstance()->GetSiteInfo().is_pdf());
+ ASSERT_TRUE(host()->GetProcess()->IsPdf());
+
+ EXPECT_EQ(BindStorageAccessHandleInFrame(host()),
+ expected_restricted_handle_result());
+}
+
IN_PROC_BROWSER_TEST_P(StorageAccessBrowserTest, BindDomStorage) {
EXPECT_EQ(BindDomStorage(), is_cookie_access_allowed());
}
Original Bug Report
StorageAccessHandle bypasses PDF renderer origin-data authorization for BroadcastChannel
VULNERABILITY DETAILS
StorageAccessHandle::Create() accepts a frame-scoped Mojo receiver after
checking storage-context restrictions and cookie access, but it does not verify
that the renderer process is authorized to access data for the frame’s committed
origin.
This is security-sensitive for PDF frames. A PDF content frame commits the
original PDF URL and receives a browser-owned StorageKey, while
ChildProcessSecurityPolicy intentionally denies the sandboxed PDF renderer
access to storage and other data for that origin.
The attacker controls a PDF hosted at origin A and has code execution in the
non-extension --pdf-renderer content process that renders it
(ExtensionProcess=0). A first-party page at the same origin A concurrently
uses a known BroadcastChannel name. Although both documents name origin A,
Chrome’s PDF-specific policy denies that renderer process access to A’s origin
data.
The current browser path is:
- the frame
BrowserInterfaceBrokerexposesblink::mojom::StorageAccessHandleto the PDF frame; StorageAccessHandle::Create()checksIsStorageAccessRestricted()andIsFullCookieAccessAllowed(), but notCanAccessDataForOrigin();StorageAccessHandle::BindBroadcastChannel()callsStorageKey::CreateFirstParty()using the PDF frame’s committed origin;- this changes the embedded PDF’s key from
A/^0BtoA/^0A; and BroadcastChannelServiceroutes it together with the victim’s first-partyA/^0Aendpoint, with no later PDF-process authorization check.
The compromised PDF renderer can consequently receive live values sent on that same-origin first-party page’s known BroadcastChannel and send forged messages back to it.
The attached renderer patch demonstrates the boundary with two channels in the
same PDF process. The ordinary channel retains A/^0B and does not receive the
victim value. The StorageAccessHandle channel is rewritten by the browser to
A/^0A, receives the value, and returns an exact forged reply. The victim
generates a fresh secret in page memory; its plaintext is never placed in the
Python server or URL. The server sees only SHA-256 values and reports success
after the exact reply is received.
A normal MojoJS page does not execute in the real --pdf-renderer content RFH
and therefore cannot exercise that RFH’s frame-scoped broker. The renderer
patch is required to model code execution in the sandboxed PDF renderer. It is
guarded by the PDF process switch and does not modify the browser receiver,
patch a test, or disable a mitigation.
Root cause and expected behavior
The missing authorization check is in:
content/browser/storage_access/storage_access_handle.cc
Before constructing the handle, the browser should reject a process for which
ChildProcessSecurityPolicy::CanAccessDataForOrigin(process_id, committed_origin) is false. Adding that browser-only check terminated the PDF
request before it received the secret, while an ordinary first-party HTML
BroadcastChannel still worked.
VERSION
Chrome Version: 154.0.8037.0 Beta
Chromium commit: e967b7961d80199276ce493904c95f5fbbc42fca
Operating System: Ubuntu 26.04 LTS, x86_64
storage_access_handle.cc, both BroadcastChannel implementation files, and
storage_key.cc are byte-identical in tag 154.0.8037.17
(62d2fcb41a84e4dcefd8c4da7dfa534e6c482854) and Chromium main
2ecb44330f0b5c5d8bd009cfbb5eaaaf1ec497ee, checked on 2026-09-12. The main
tree also retains the frame binder and the PDF-specific origin-data denial.
REPRODUCTION CASE
Use a clean checkout and apply the two-file renderer patch:
cd /path/to/chromium/src
git checkout --detach e967b7961d80199276ce493904c95f5fbbc42fca
gclient sync -D
git apply /path/to/poc.patch
gn gen out/vrp --args='is_debug=false is_component_build=true is_official_build=false symbol_level=0 blink_symbol_level=0 v8_symbol_level=0 use_remoteexec=false'
autoninja -C out/vrp chrome
Start the attached Python server in one terminal:
python3 /path/to/poc.py
Launch the built Chrome separately from a graphical desktop terminal with a fresh profile:
VRP_PROFILE=$(mktemp -d)
out/vrp/chrome \
--user-data-dir="$VRP_PROFILE" \
--no-first-run \
--no-default-browser-check \
http://127.0.0.1:18080/victim.html \
http://localhost:18080/attacker.html
No interaction is required. After the PDF loads, the server prints:
[EXPECTED_SHA256] <random digest>
[FORGED_ACK_SHA256] <the same digest>
[NEGATIVE_CONTROL] ordinary PDF channel received no secret
[PASS] PDF StorageAccessHandle reached the first-party origin channel
The victim tab also changes its text to:
PASS: PDF renderer returned the victim-only same-origin value
The exact final attachment pair passed 3/3 with fresh profiles and three
distinct digests. During a separate process-state check, the responding PDF
process had ExtensionProcess=0, NoNewPrivs=1, and Seccomp=2.
For an ordinary first-party positive control, restart poc.py and launch a
fresh profile with these two URLs instead:
http://127.0.0.1:18080/victim.html
http://127.0.0.1:18080/control.html
The server prints [PASS] ordinary first-party control.
ATTACHMENTS
poc.pypoc.patch
CREDIT INFORMATION
Reporter credit: Hoyong Jin