Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Fenced Frames
DescriptionInsufficient policy enforcement in Fenced Frames
ComponentFenced Frames
Bug ClassLogic Error
Tracker487564032
Fix commitf7d4aed5f742 (chromium/src) +59/-8
CISA KEVNot listed
CreditedTianyi Hu
Disclosed2026-06-02

Files Changed

  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/security_exploit_browsertest.cc
From f7d4aed5f74244c2f81129f4b1a1e9d953c8b269 Mon Sep 17 00:00:00 2001
From: Tianyi Hu <oscarhuthu@gmail.com>
Date: Thu, 09 Apr 2026 08:15:44 -0700
Subject: [PATCH] Use opaque initiator_origin for _unfencedTop navigations

RenderFrameHostImpl::OpenURL() forwards the renderer-supplied
params->initiator_origin to NavigateFromFrameProxy() for
_unfencedTop navigations from fenced frames. The code contains
a commented-out declaration (url::Origin initiator_origin;) that
was intended to replace the renderer value with an opaque origin,
but was never activated.

Uncomment the opaque origin declaration and pass it to
NavigateFromFrameProxy() instead of params->initiator_origin.
A default-constructed url::Origin is opaque, so the destination
will see Sec-Fetch-Site: cross-site — matching the fenced frame
privacy model requirement. Non-fenced-frame navigations are
unaffected because they do not enter the is_unfenced_top_navigation
code path.

Bug: 487564032, 40221940
Change-Id: I7d5767f345e90e6dadf8d9c2f39c71d15cff6103
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7607320
Commit-Queue: Xiaochen Zhou <xiaochenzh@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Reviewed-by: Xiaochen Zhou <xiaochenzh@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1612246}
---

diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index e89e4c1e..e0bdcb8 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -9990,13 +9990,14 @@
     // current entry but don't, due to this line.
     bool should_replace_current_entry = false;
 
-    // TODO(crbug.com/40221940): Null out the initiator origin, frame token, and
-    // site instance.
-    // We use an opaque `initiator_origin` in order to avoid leaking
-    // information from the fenced frame to its embedder. (The navigation will
-    // be treated as cross-origin unconditionally.) We don't need to provide a
-    // `source_site_instance`.
-    // url::Origin initiator_origin;
+    // Use an opaque initiator_origin to prevent the fenced frame's
+    // origin from leaking to the destination of the _unfencedTop
+    // navigation via Sec-Fetch-Site and related request headers.
+    // The fenced frame privacy model requires that the embedder
+    // cannot identify the content of the fenced frame; passing the
+    // renderer-supplied params->initiator_origin would violate this.
+    // See crbug.com/40221940.
+    url::Origin initiator_origin;
 
     // TODO(crbug.com/40193166): Resolve the discussion of download policy.
     blink::NavigationDownloadPolicy download_policy;
@@ -10007,7 +10008,7 @@
     target_frame->frame_tree_node()->navigator().NavigateFromFrameProxy(
         target_frame, validated_params_url,
         base::OptionalToPtr(params->initiator_frame_token),
-        GetProcess()->GetDeprecatedID(), params->initiator_origin,
+        GetProcess()->GetDeprecatedID(), initiator_origin,
         params->initiator_base_url, GetSiteInstance(), content::Referrer(),
         ui::PAGE_TRANSITION_LINK, should_replace_current_entry, download_policy,
         "GET",
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index 5518ee4..f20a708 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -3113,6 +3113,56 @@
   EXPECT_EQ(bad_message::RFH_FOCUS_ACROSS_FENCED_BOUNDARY, kill_waiter.Wait());
 }
 
+// Regression test for crbug.com/40221940: a _unfencedTop navigation from a
+// fenced frame must use an opaque initiator_origin. Previously the browser
+// forwarded the renderer-supplied initiator_origin to NavigateFromFrameProxy(),
+// leaking the fenced frame's identity to the destination via Sec-Fetch-Site.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames,
+                       UnfencedTopNavigationHasOpaqueInitiatorOrigin) {
+  GURL main_url(https_server()->GetURL("a.test", "/simple_page.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  // Create a fenced frame and get its inner root RFH. Use a URL under
+  // /fenced_frames/ which has the Supports-Loading-Mode header.
+  GURL fenced_url(
+      https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
+  RenderFrameHost* fenced_rfh_generic =
+      fenced_frame_test_helper().CreateFencedFrame(
+          shell()->web_contents()->GetPrimaryMainFrame(), fenced_url);
+  ASSERT_TRUE(fenced_rfh_generic);
+  RenderFrameHostImpl* fenced_rfh =
+      static_cast<RenderFrameHostImpl*>(fenced_rfh_generic);
+
+  // The destination URL for the _unfencedTop navigation.
+  GURL dest_url(https_server()->GetURL("b.test", "/simple_page.html"));
+
+  // Watch for the navigation to start so we can inspect the initiator origin.
+  TestNavigationManager nav_manager(shell()->web_contents(), dest_url);
+
+  // Inject an OpenURL IPC from the fenced frame with
+  // is_unfenced_top_navigation=true and a non-opaque initiator_origin.
+  // A compromised renderer can set initiator_origin to any origin its
+  // process hosts; VerifyInitiatorOrigin() will accept it.
+  auto params = CreateOpenURLParams(dest_url);
+  params->is_unfenced_top_navigation = true;
+  params->initiator_origin = url::Origin::Create(fenced_url);
+  ASSERT_FALSE(params->initiator_origin.opaque());
+  static_cast<mojom::FrameHost*>(fenced_rfh)->OpenURL(std::move(params));
+
+  // Wait for the navigation to reach the network.
+  ASSERT_TRUE(nav_manager.WaitForRequestStart());
+
+  // The fix replaces the renderer-supplied initiator_origin with an opaque
+  // origin before passing it to NavigateFromFrameProxy(). Verify that the
+  // navigation handle reflects the opaque origin.
+  NavigationHandle* handle = nav_manager.GetNavigationHandle();
+  ASSERT_TRUE(handle);
+  ASSERT_TRUE(handle->GetInitiatorOrigin().has_value());
+  EXPECT_TRUE(handle->GetInitiatorOrigin()->opaque())
+      << "Expected opaque initiator origin for _unfencedTop navigation, got: "
+      << handle->GetInitiatorOrigin()->Serialize();
+}
+
 namespace {
 
 // Interceptor that replaces the origin in the DidCommitProvisionalLoadParams
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index 5518ee4..f20a708 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -3113,6 +3113,56 @@
   EXPECT_EQ(bad_message::RFH_FOCUS_ACROSS_FENCED_BOUNDARY, kill_waiter.Wait());
 }
 
+// Regression test for crbug.com/40221940: a _unfencedTop navigation from a
+// fenced frame must use an opaque initiator_origin. Previously the browser
+// forwarded the renderer-supplied initiator_origin to NavigateFromFrameProxy(),
+// leaking the fenced frame's identity to the destination via Sec-Fetch-Site.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames,
+                       UnfencedTopNavigationHasOpaqueInitiatorOrigin) {
+  GURL main_url(https_server()->GetURL("a.test", "/simple_page.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  // Create a fenced frame and get its inner root RFH. Use a URL under
+  // /fenced_frames/ which has the Supports-Loading-Mode header.
+  GURL fenced_url(
+      https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
+  RenderFrameHost* fenced_rfh_generic =
+      fenced_frame_test_helper().CreateFencedFrame(
+          shell()->web_contents()->GetPrimaryMainFrame(), fenced_url);
+  ASSERT_TRUE(fenced_rfh_generic);
+  RenderFrameHostImpl* fenced_rfh =
+      static_cast<RenderFrameHostImpl*>(fenced_rfh_generic);
+
+  // The destination URL for the _unfencedTop navigation.
+  GURL dest_url(https_server()->GetURL("b.test", "/simple_page.html"));
+
+  // Watch for the navigation to start so we can inspect the initiator origin.
+  TestNavigationManager nav_manager(shell()->web_contents(), dest_url);
+
+  // Inject an OpenURL IPC from the fenced frame with
+  // is_unfenced_top_navigation=true and a non-opaque initiator_origin.
+  // A compromised renderer can set initiator_origin to any origin its
+  // process hosts; VerifyInitiatorOrigin() will accept it.
+  auto params = CreateOpenURLParams(dest_url);
+  params->is_unfenced_top_navigation = true;
+  params->initiator_origin = url::Origin::Create(fenced_url);
+  ASSERT_FALSE(params->initiator_origin.opaque());
+  static_cast<mojom::FrameHost*>(fenced_rfh)->OpenURL(std::move(params));
+
+  // Wait for the navigation to reach the network.
+  ASSERT_TRUE(nav_manager.WaitForRequestStart());
+
+  // The fix replaces the renderer-supplied initiator_origin with an opaque
+  // origin before passing it to NavigateFromFrameProxy(). Verify that the
+  // navigation handle reflects the opaque origin.
+  NavigationHandle* handle = nav_manager.GetNavigationHandle();
+  ASSERT_TRUE(handle);
+  ASSERT_TRUE(handle->GetInitiatorOrigin().has_value());
+  EXPECT_TRUE(handle->GetInitiatorOrigin()->opaque())
+      << "Expected opaque initiator origin for _unfencedTop navigation, got: "
+      << handle->GetInitiatorOrigin()->Serialize();
+}
+
 namespace {
 
 // Interceptor that replaces the origin in the DidCommitProvisionalLoadParams
Loading diff…

Original Bug Report

reported by os...@gmail.com

Fenced frame `_unfencedTop` navigation leaks `initiator_origin` to destination via `Sec-Fetch-Site`


Report description

Fenced frame _unfencedTop navigation leaks initiator_origin to destination via Sec-Fetch-Site


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/render_frame_host_impl.cc


The problem

Please describe the technical details of the vulnerability

RenderFrameHostImpl::OpenURL() contains a commented-out TODO (crbug.com/40221940) that was supposed to replace the renderer-supplied initiator_origin with an opaque origin before forwarding a _unfencedTop navigation to NavigateFromFrameProxy(). The fix was never completed: // url::Origin initiator_origin; remains commented out, and params->initiator_origin — fully controlled by the renderer — is passed directly to NavigateFromFrameProxy().

A compromised fenced frame renderer can set initiator_origin to any origin its process hosts, causing the browser to forward the fenced frame’s real identity to the destination server via Sec-Fetch-Site, violating the Privacy Sandbox isolation requirement.

Vulnerable files:

Vulnerable code:

// In RenderFrameHostImpl::OpenURL(), inside the is_unfenced_top_navigation block:

// TODO(crbug.com/40221940): Null out the initiator origin, frame token, and
// site instance.
// We use an opaque `initiator_origin` in order to avoid leaking
// information from the fenced frame to its embedder. (The navigation will
// be treated as cross-origin unconditionally.) We don't need to provide a
// `source_site_instance`.
// url::Origin initiator_origin;    // <── FIX IS COMMENTED OUT

// ...

target_frame->frame_tree_node()->navigator().NavigateFromFrameProxy(
    ..., params->initiator_origin,  // <── RENDERER VALUE FORWARDED UNCHANGED
    ..., /*is_unfenced_top_navigation=*/true, ...);

Transmission chain:

  1. Fenced frame renderer calls RenderFrameImpl::OpenURL() with is_unfenced_top_navigation=true.
  2. Renderer sets params->initiator_origin = info->url_request.RequestorOrigin() — the real fenced frame origin. A compromised renderer can set this to any origin its process hosts.
  3. params is sent to the browser via the LocalFrameHost::OpenURL Mojo IPC (OpenURLParams struct in remote_frame.mojom).
  4. In RenderFrameHostImpl::OpenURL(), the browser validates the origin with VerifyInitiatorOrigin() (passes, since the process does host the origin), then enters the _unfencedTop code path.
  5. The TODO fix (url::Origin initiator_origin;) is commented out and has no effect.
  6. params->initiator_origin (the real fenced frame origin) is passed directly to NavigateFromFrameProxy(), which stores it in FrameNavigationEntry and forwards it in CommonNavigationParams.
  7. The network service sets Sec-Fetch-Site on the outgoing request using the stored initiator origin. If the fenced frame and destination share the same origin, Chrome sets Sec-Fetch-Site: same-origin instead of the required cross-site.

Steps to reproduce:

  1. git apply m2_poc_patch.diff in the stable checkout
  2. autoninja -C out/Default chrome -j8
  3. Place index.html, fenced.html, and serve.py in the same directory, then start the HTTP server:
    • python3 serve.py
  4. Launch patched Chrome and observe:
    • out/Default/Chromium.app/Contents/MacOS/Chromium --enable-blink-features=FencedFramesDefaultMode --user-data-dir=/tmp/chrome-m2-test http://localhost:8080/index.html
    • After ~2 seconds, server stdout shows: [*] /destination Sec-Fetch-Site: same-origin
    • The fenced frame’s origin has leaked. Privacy Sandbox isolation is violated.

m2_poc_patch.diff simulates a compromised renderer by forging user_gesture=true for _unfencedTop navigations, bypassing ValidateUnfencedTopNavigation() so the navigation proceeds without user interaction. The renderer’s real initiator_origin (the fenced frame’s origin) flows through unchanged — the bug is that the browser forwards it to NavigateFromFrameProxy() instead of replacing it with an opaque origin. FencedFramesDefaultMode enables the FencedFrameConfig URL constructor for testing; in production, fenced frames are created via Protected Audience or Shared Storage APIs.

Bisect:

Introducing commit: a42fdef8ad1898f97e34eaf8b74f0d3a82361d29

Evidence:

  • M104 (104.0.5112.0): "Null out the initiator origin" pattern is absent (grep count = 0)
  • M105 (105.0.5195.0): pattern is present (grep count = 1)
  • Parent commit (a42fdef^): pattern absent — the diff shows these lines being added
  • Commit a42fdef: pattern present — introduces the TODO + commented-out url::Origin initiator_origin;

Earliest affected: Chrome M105 (stable August 30, 2022). Latest confirmed: Chrome M145 (145.0.7632.117, current Stable). Vulnerable for ~3.5 years.

Suggested fix:

m2_fix.diff is attached. Uncomment and activate the opaque origin, then use it instead of params->initiator_origin in the NavigateFromFrameProxy() call:

-    // TODO(crbug.com/40221940): Null out the initiator origin, ...
-    // url::Origin initiator_origin;
+    url::Origin initiator_origin;

-        GetProcess()->GetDeprecatedID(), params->initiator_origin,
+        GetProcess()->GetDeprecatedID(), initiator_origin,

A default-constructed url::Origin is opaque, so the destination server will see Sec-Fetch-Site: cross-site — matching the Privacy Sandbox requirement. Normal (non-fenced-frame) navigations are unaffected because they do not enter the is_unfenced_top_navigation code path.

I will upload a Gerrit CL and add the link in a follow-up comment.

Impact analysis

A compromised renderer inside a fenced frame (e.g., via renderer RCE in a third-party ad delivered via Protected Audience/FLEDGE) can set initiator_origin to its real origin. The browser forwards this to the destination server via Sec-Fetch-Site, revealing the fenced frame’s content identity.

The Privacy Sandbox guarantee for fenced frames is that the embedding page and the fenced frame cannot identify each other. Leaking the fenced frame’s origin to the destination of a _unfencedTop navigation breaks this by:

  • Allowing the destination server to identify what ad/content was displayed in the fenced frame
  • Enabling cross-context correlation that the Privacy Sandbox is specifically designed to prevent
  • Leaking via standard HTTP headers (Sec-Fetch-Site, Referer if applicable) that are routinely logged

The Chromium code itself acknowledges this is a bug (crbug.com/40221940 appears three times in the same function) and documents the intended fix — it was simply never activated.

Exploitation requires a compromised renderer (renderer RCE), which is a standard assumption in Chrome’s security model.


The cause

What version of Chrome have you found the security issue in?

145.0.7632.117 (Stable, current as of February 2026)

No, it is not related to a crash.

Choose the type of vulnerability

Site Isolation Bypass

How would you like to be publicly acknowledged for your report?

Tianyi Hu

View on issue tracker