Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in SiteIsolation
DescriptionMemory corruption in SiteIsolation
ComponentSiteIsolation
Sub-componentSAA
Bug ClassMemory Corruption
Tracker561099700
Fix commit7a02fbd10637 (chromium/src) +24/-1
CISA KEVNot listed
CreditedHoyong Jin
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
IN_PROC_BROWSER_TEST_P
content/browser/storage_access/storage_access_browsertest.cc
modified

Files Changed

  • content/browser/storage_access/storage_access_browsertest.cc
  • content/browser/renderer_host/render_frame_host_impl.cc
From 7a02fbd106378d4d3b66ca6f7a980003f00aa1be Mon Sep 17 00:00:00 2001
From: Chris Fredrickson <cfredric@chromium.org>
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 <alexmos@chromium.org>
Auto-Submit: Chris Fredrickson <cfredric@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
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());
 }
Loading diff…

Regression Test / PoC

shipped with the fix
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());
 }
Loading diff…

Original Bug Report

reported by ma...@gmail.com

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:

  1. the frame BrowserInterfaceBroker exposes blink::mojom::StorageAccessHandle to the PDF frame;
  2. StorageAccessHandle::Create() checks IsStorageAccessRestricted() and IsFullCookieAccessAllowed(), but not CanAccessDataForOrigin();
  3. StorageAccessHandle::BindBroadcastChannel() calls StorageKey::CreateFirstParty() using the PDF frame’s committed origin;
  4. this changes the embedded PDF’s key from A/^0B to A/^0A; and
  5. BroadcastChannelService routes it together with the victim’s first-party A/^0A endpoint, 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.py
  • poc.patch

CREDIT INFORMATION

Reporter credit: Hoyong Jin

View on issue tracker