Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Navigation
DescriptionMissing authorization in Navigation
ComponentNavigation
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker514072284
Fix commitaa536593b9a6 (chromium/src) +128/-7
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-05-17 (per vendor advisory)
Fix landed2026-08-26 (public git)
Disclosed2026-09-22
Patch gap27 days — fix public in source before the advisory

Background

`DidBlockNavigation`
an IPC handler on RenderFrameHostImpl invoked by a renderer to report that a navigation (typically a redirect without user gesture) was blocked, which surfaces blocked-redirect UI on the tab.
Back/forward cache (bfcache)
a mechanism that keeps a fully-constructed but non-active document alive after the user navigates away, so its RenderFrameHost can still emit in-flight IPCs.
Fenced frame
an embedded frame with an enforced boundary from its embedder, whose nested documents legitimately trigger navigation-blocking IPCs but must not drive embedder-tab UI.
`IsActive` / `IsNestedWithinFencedFrame`
RenderFrameHostImpl predicates that report, respectively, whether the frame host belongs to the active/primary document and whether it is nested inside a fenced frame tree.

Root Cause Analysis

RenderFrameHostImpl::DidBlockNavigation accepted the renderer-originated notification unconditionally and forwarded it to delegate_->OnDidBlockNavigation, which drives tab-level blocked-redirect UI keyed off the primary main frame’s committed URL and origin. Because the handler never checked the lifecycle or fenced-frame status of the reporting RenderFrameHost, a notification from a back/forward-cached (non-active) document or from a document IsNestedWithinFencedFrame would race lifecycle transitions or fire legitimately, yet incorrectly surface blocked-redirect UI attributed to the primary tab. The invariant violated is that only the active, non-fenced document should influence primary-tab navigation UI.

The fix adds an early if (!IsActive() || IsNestedWithinFencedFrame()) { return; } guard that silently drops such notifications, matching sibling LocalFrameHost handlers; it also removes the now-unnecessary DCHECK_NE against LifecycleStateImpl::kPrerendering, since non-active documents are filtered before reaching that point.

Key insight
The core mistake was that DidBlockNavigation trusted every renderer notification without confirming the reporting frame was the active, non-fenced document, letting non-active or fenced RenderFrameHosts spoof tab-level UI; the fix gates the handler on IsActive() and !IsNestedWithinFencedFrame().

Attack Path

  1. Set up a fenced frame An embedder page (or attacker page hosting content) creates a fencedframe and navigates it to a document under attacker influence, establishing an IsNestedWithinFencedFrame RenderFrameHost.
  2. Trigger a blocked navigation inside the boundary The nested document performs a navigation that the browser blocks (for example a redirect with no user gesture, NavigationBlockedReason::kRedirectWithNoUserGesture), causing the renderer to send DidBlockNavigation.
  3. Cross the boundary into embedder UI The unguarded handler forwards to delegate_->OnDidBlockNavigation using the primary frame’s GetLastCommittedURL and GetLastCommittedOrigin, so blocked-redirect UI appears on the embedder’s primary tab despite originating in the fenced (or back/forward-cached, non-active) frame.
  4. Misattribute UI to the top-level tab (inferred) The resulting spoofed or misattributed security indicator on the primary tab could mislead the user about the tab’s true navigation state. (inferred)

Impact Assessment

An attacker controlling a fenced-frame or back/forward-cached document gains the ability to surface blocked-redirect UI misattributed to the embedder’s primary tab, a browser-process UI/state confusion triggered from the renderer without proper authorization. The precondition is the ability to run script in a fenced frame or leave a bfcached document that emits DidBlockNavigation. The advisory classifies this as a LogicError (missing authorization in Navigation); it states no exploited-in-the-wild status, and any escalation beyond misattributed navigation UI is not demonstrated by the patch (inferred).

Changed Functions

FunctionChangeNotes
BlockedNavigationDelegate
content/browser/fenced_frame/fenced_frame_browsertest.cc
modified
FencedFrameBrowserTestBase
content/browser/fenced_frame/fenced_frame_browsertest.cc
modified
UUIDFrameTreeBrowserTest
content/browser/fenced_frame/fenced_frame_browsertest.cc
modified
BlockedNavigationDelegate
content/browser/renderer_host/render_frame_host_impl_browsertest.cc
modified
RenderFrameHostImplPrerenderBrowserTest
content/browser/renderer_host/render_frame_host_impl_browsertest.cc
modified

Files Changed

  • content/browser/fenced_frame/fenced_frame_browsertest.cc
  • content/browser/renderer_host/render_frame_host_impl_browsertest.cc
  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/public/browser/prerender_web_contents_delegate.cc

Audit Directions

  • Renderer IPC handlers lacking lifecycle guards
    Audit RenderFrameHostImpl IPC handlers that forward to delegate_ for missing IsActive() / IsNestedWithinFencedFrame() / bfcache checks before affecting primary-tab UI or state.
  • Fenced-frame boundary leaks
    Look for handlers that read GetLastCommittedURL/GetLastCommittedOrigin of the primary frame in response to a nested or fenced RenderFrameHost’s notification, crossing the fenced-frame trust boundary.
  • DCHECK-only invariants
    Find places relying on a DCHECK/NOTREACHED to assert a frame is not prerendering or non-active, where release builds silently proceed instead of dropping the notification.
From aa536593b9a6db357c47a9588c150b71137ebe87 Mon Sep 17 00:00:00 2001
From: Arthur Sonzogni <arthursonzogni@chromium.org>
Date: Wed, 26 Aug 2026 16:10:59 -0700
Subject: [PATCH] Ignore DidBlockNavigation from non-active or fenced RFHs

DidBlockNavigation notifications from back/forward cached documents or
fenced frames incorrectly surface blocked-redirect UI on the primary
tab.

Drop these notifications when the RenderFrameHost is not active or is
nested in a fenced frame, matching sibling LocalFrameHost handlers.

Bug: 514072284
Change-Id: I49889f2e4b9fd09bc2d5c115ca9cb5ea5321ba31
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8295459
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Auto-Submit: Arthur Sonzogni <arthursonzogni@chromium.org>
Commit-Queue: Alex Moshchuk <alexmos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1686761}
---
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 101aff0..ac2c1bf 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -8965,16 +8965,18 @@
 void RenderFrameHostImpl::DidBlockNavigation(
     const GURL& blocked_url,
     blink::mojom::NavigationBlockedReason reason) {
+  // Silently drop notifications from inactive or fenced frames. In-flight IPCs
+  // can race with lifecycle transitions, and subframes in fenced frames trigger
+  // this IPC legitimately without expecting tab-level UI.
+  if (!IsActive() || IsNestedWithinFencedFrame()) {
+    return;
+  }
+
   // Do not allow renderers to show off-limits URLs in the blocked dialog.
   GURL validated_blocked_url = blocked_url;
   RenderProcessHost* process = GetProcess();
   process->FilterURL(/*empty_allowed=*/false, &validated_blocked_url);
 
-  // Cross-origin navigations are not allowed in prerendering so we can not
-  // reach here while prerendering.
-  // TODO(522986874): CHECK-exclusion: Convert to a CHECK once we are confident
-  // it won't be triggered.
-  DCHECK_NE(lifecycle_state(), LifecycleStateImpl::kPrerendering);
   delegate_->OnDidBlockNavigation(validated_blocked_url, GetLastCommittedURL(),
                                   GetLastCommittedOrigin(), reason);
 }
diff --git a/content/public/browser/prerender_web_contents_delegate.cc b/content/public/browser/prerender_web_contents_delegate.cc
index 992b4ba9b..f287124 100644
--- a/content/public/browser/prerender_web_contents_delegate.cc
+++ b/content/public/browser/prerender_web_contents_delegate.cc
@@ -112,8 +112,8 @@
     const GURL& initiator_url,
     const url::Origin& initiator_origin,
     blink::mojom::NavigationBlockedReason reason) {
-  // DCHECK against LifecycleState in RenderFrameHostImpl::DidBlockNavigation()
-  // ensures this is never called during prerendering.
+  // RenderFrameHostImpl::DidBlockNavigation() ignores notifications from
+  // non-active documents, so this is never called during prerendering.
   NOTREACHED();
 }
 
diff --git a/content/browser/fenced_frame/fenced_frame_browsertest.cc b/content/browser/fenced_frame/fenced_frame_browsertest.cc
index 8da392c..6a00f26 100644
--- a/content/browser/fenced_frame/fenced_frame_browsertest.cc
+++ b/content/browser/fenced_frame/fenced_frame_browsertest.cc
@@ -112,6 +112,31 @@
   return pending_urn.value();
 }
 
+class BlockedNavigationDelegate : public WebContentsDelegate {
+ public:
+  explicit BlockedNavigationDelegate(WebContents* web_contents)
+      : web_contents_(web_contents) {
+    web_contents_->SetDelegate(this);
+  }
+
+  ~BlockedNavigationDelegate() override { web_contents_->SetDelegate(nullptr); }
+
+  void OnDidBlockNavigation(
+      WebContents* web_contents,
+      const GURL& blocked_url,
+      const GURL& initiator_url,
+      const url::Origin& initiator_origin,
+      blink::mojom::NavigationBlockedReason reason) override {
+    ++blocked_navigation_count_;
+  }
+
+  int blocked_navigation_count() const { return blocked_navigation_count_; }
+
+ private:
+  raw_ptr<WebContents> web_contents_;
+  int blocked_navigation_count_ = 0;
+};
+
 }  // namespace
 
 class FencedFrameBrowserTestBase : public ContentBrowserTest {
@@ -5588,6 +5613,45 @@
   }
 }
 
+// Tests that a fenced frame reporting a blocked redirect does not surface
+// blocked-redirect UI on the embedder's tab.
+IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest,
+                       DidBlockNavigationIgnoredFromFencedFrame) {
+  GURL main_url(https_server()->GetURL("a.test", "/hello.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+  FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
+
+  EXPECT_TRUE(ExecJs(root,
+                     "var fenced_frame = document.createElement('fencedframe');"
+                     "document.body.appendChild(fenced_frame);"));
+  ASSERT_EQ(1U, root->child_count());
+  FrameTreeNode* fenced_frame_root_node =
+      GetFencedFrameRootNode(root->child_at(0));
+
+  GURL fenced_frame_url(
+      https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
+  std::string navigate_script =
+      JsReplace("fenced_frame.config = new FencedFrameConfig($1);",
+                fenced_frame_url.spec());
+  NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
+      fenced_frame_root_node, navigate_script);
+
+  BlockedNavigationDelegate delegate(web_contents());
+
+  // Simulate the fenced frame document reporting a blocked redirect.
+  fenced_frame_root_node->current_frame_host()->DidBlockNavigation(
+      main_url,
+      blink::mojom::NavigationBlockedReason::kRedirectWithNoUserGesture);
+  EXPECT_EQ(0, delegate.blocked_navigation_count());
+
+  // The embedder's primary main frame should still be able to report blocked
+  // redirects.
+  primary_main_frame_host()->DidBlockNavigation(
+      main_url,
+      blink::mojom::NavigationBlockedReason::kRedirectWithNoUserGesture);
+  EXPECT_EQ(1, delegate.blocked_navigation_count());
+}
+
 // Parameterized on whether the feature is enabled or not.
 class UUIDFrameTreeBrowserTest
     : public FencedFrameBrowserTestBase,
diff --git a/content/browser/renderer_host/render_frame_host_impl_browsertest.cc b/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
index 6390b29..b6f5dde4 100644
--- a/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
+++ b/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
@@ -214,6 +214,31 @@
       trustmeifembeddingsecure_factory_;
 };
 
+class BlockedNavigationDelegate : public WebContentsDelegate {
+ public:
+  explicit BlockedNavigationDelegate(WebContents* web_contents)
+      : web_contents_(web_contents) {
+    web_contents_->SetDelegate(this);
+  }
+
+  ~BlockedNavigationDelegate() override { web_contents_->SetDelegate(nullptr); }
+
+  void OnDidBlockNavigation(
+      WebContents* web_contents,
+      const GURL& blocked_url,
+      const GURL& initiator_url,
+      const url::Origin& initiator_origin,
+      blink::mojom::NavigationBlockedReason reason) override {
+    ++blocked_navigation_count_;
+  }
+
+  int blocked_navigation_count() const { return blocked_navigation_count_; }
+
+ private:
+  raw_ptr<WebContents> web_contents_;
+  int blocked_navigation_count_ = 0;
+};
+
 }  // namespace
 
 // TODO(mlamouri): part of these tests were removed because they were dependent
@@ -10087,6 +10112,36 @@
   EXPECT_TRUE(ContainsSurfaceIdOrNewer(ids, id_b));
 }
 
+// Tests that DidBlockNavigation from a document that has been placed in the
+// back/forward cache does not surface blocked-redirect UI for the now-primary
+// page.
+IN_PROC_BROWSER_TEST_F(RenderFrameHostImplBrowserTestWithBFCache,
+                       DidBlockNavigationIgnoredFromBackForwardCache) {
+  GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
+
+  // Navigate to A.
+  EXPECT_TRUE(NavigateToURL(shell(), url_a));
+  RenderFrameHostImpl* rfh_a = web_contents()->GetPrimaryMainFrame();
+
+  // Navigate to B, placing A in the back/forward cache.
+  EXPECT_TRUE(NavigateToURL(shell(), url_b));
+  ASSERT_TRUE(rfh_a->IsInBackForwardCache());
+
+  BlockedNavigationDelegate delegate(web_contents());
+
+  // Simulate the cached document reporting a blocked redirect after it has
+  // entered the back/forward cache.
+  rfh_a->DidBlockNavigation(
+      url_a, blink::mojom::NavigationBlockedReason::kRedirectWithNoUserGesture);
+  EXPECT_EQ(0, delegate.blocked_navigation_count());
+
+  // The active document should still be able to report blocked redirects.
+  web_contents()->GetPrimaryMainFrame()->DidBlockNavigation(
+      url_b, blink::mojom::NavigationBlockedReason::kRedirectWithNoUserGesture);
+  EXPECT_EQ(1, delegate.blocked_navigation_count());
+}
+
 class RenderFrameHostImplPrerenderBrowserTest
     : public RenderFrameHostImplBrowserTest {
  public:
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/fenced_frame/fenced_frame_browsertest.cc b/content/browser/fenced_frame/fenced_frame_browsertest.cc
index 8da392c..6a00f26 100644
--- a/content/browser/fenced_frame/fenced_frame_browsertest.cc
+++ b/content/browser/fenced_frame/fenced_frame_browsertest.cc
@@ -112,6 +112,31 @@
   return pending_urn.value();
 }
 
+class BlockedNavigationDelegate : public WebContentsDelegate {
+ public:
+  explicit BlockedNavigationDelegate(WebContents* web_contents)
+      : web_contents_(web_contents) {
+    web_contents_->SetDelegate(this);
+  }
+
+  ~BlockedNavigationDelegate() override { web_contents_->SetDelegate(nullptr); }
+
+  void OnDidBlockNavigation(
+      WebContents* web_contents,
+      const GURL& blocked_url,
+      const GURL& initiator_url,
+      const url::Origin& initiator_origin,
+      blink::mojom::NavigationBlockedReason reason) override {
+    ++blocked_navigation_count_;
+  }
+
+  int blocked_navigation_count() const { return blocked_navigation_count_; }
+
+ private:
+  raw_ptr<WebContents> web_contents_;
+  int blocked_navigation_count_ = 0;
+};
+
 }  // namespace
 
 class FencedFrameBrowserTestBase : public ContentBrowserTest {
@@ -5588,6 +5613,45 @@
   }
 }
 
+// Tests that a fenced frame reporting a blocked redirect does not surface
+// blocked-redirect UI on the embedder's tab.
+IN_PROC_BROWSER_TEST_F(FencedFrameParameterizedBrowserTest,
+                       DidBlockNavigationIgnoredFromFencedFrame) {
+  GURL main_url(https_server()->GetURL("a.test", "/hello.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+  FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
+
+  EXPECT_TRUE(ExecJs(root,
+                     "var fenced_frame = document.createElement('fencedframe');"
+                     "document.body.appendChild(fenced_frame);"));
+  ASSERT_EQ(1U, root->child_count());
+  FrameTreeNode* fenced_frame_root_node =
+      GetFencedFrameRootNode(root->child_at(0));
+
+  GURL fenced_frame_url(
+      https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
+  std::string navigate_script =
+      JsReplace("fenced_frame.config = new FencedFrameConfig($1);",
+                fenced_frame_url.spec());
+  NavigateFrameInsideFencedFrameTreeAndWaitForFinishedLoad(
+      fenced_frame_root_node, navigate_script);
+
+  BlockedNavigationDelegate delegate(web_contents());
+
+  // Simulate the fenced frame document reporting a blocked redirect.
+  fenced_frame_root_node->current_frame_host()->DidBlockNavigation(
+      main_url,
+      blink::mojom::NavigationBlockedReason::kRedirectWithNoUserGesture);
+  EXPECT_EQ(0, delegate.blocked_navigation_count());
+
+  // The embedder's primary main frame should still be able to report blocked
+  // redirects.
+  primary_main_frame_host()->DidBlockNavigation(
+      main_url,
+      blink::mojom::NavigationBlockedReason::kRedirectWithNoUserGesture);
+  EXPECT_EQ(1, delegate.blocked_navigation_count());
+}
+
 // Parameterized on whether the feature is enabled or not.
 class UUIDFrameTreeBrowserTest
     : public FencedFrameBrowserTestBase,
diff --git a/content/browser/renderer_host/render_frame_host_impl_browsertest.cc b/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
index 6390b29..b6f5dde4 100644
--- a/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
+++ b/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
@@ -214,6 +214,31 @@
       trustmeifembeddingsecure_factory_;
 };
 
+class BlockedNavigationDelegate : public WebContentsDelegate {
+ public:
+  explicit BlockedNavigationDelegate(WebContents* web_contents)
+      : web_contents_(web_contents) {
+    web_contents_->SetDelegate(this);
+  }
+
+  ~BlockedNavigationDelegate() override { web_contents_->SetDelegate(nullptr); }
+
+  void OnDidBlockNavigation(
+      WebContents* web_contents,
+      const GURL& blocked_url,
+      const GURL& initiator_url,
+      const url::Origin& initiator_origin,
+      blink::mojom::NavigationBlockedReason reason) override {
+    ++blocked_navigation_count_;
+  }
+
+  int blocked_navigation_count() const { return blocked_navigation_count_; }
+
+ private:
+  raw_ptr<WebContents> web_contents_;
+  int blocked_navigation_count_ = 0;
+};
+
 }  // namespace
 
 // TODO(mlamouri): part of these tests were removed because they were dependent
@@ -10087,6 +10112,36 @@
   EXPECT_TRUE(ContainsSurfaceIdOrNewer(ids, id_b));
 }
 
+// Tests that DidBlockNavigation from a document that has been placed in the
+// back/forward cache does not surface blocked-redirect UI for the now-primary
+// page.
+IN_PROC_BROWSER_TEST_F(RenderFrameHostImplBrowserTestWithBFCache,
+                       DidBlockNavigationIgnoredFromBackForwardCache) {
+  GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
+
+  // Navigate to A.
+  EXPECT_TRUE(NavigateToURL(shell(), url_a));
+  RenderFrameHostImpl* rfh_a = web_contents()->GetPrimaryMainFrame();
+
+  // Navigate to B, placing A in the back/forward cache.
+  EXPECT_TRUE(NavigateToURL(shell(), url_b));
+  ASSERT_TRUE(rfh_a->IsInBackForwardCache());
+
+  BlockedNavigationDelegate delegate(web_contents());
+
+  // Simulate the cached document reporting a blocked redirect after it has
+  // entered the back/forward cache.
+  rfh_a->DidBlockNavigation(
+      url_a, blink::mojom::NavigationBlockedReason::kRedirectWithNoUserGesture);
+  EXPECT_EQ(0, delegate.blocked_navigation_count());
+
+  // The active document should still be able to report blocked redirects.
+  web_contents()->GetPrimaryMainFrame()->DidBlockNavigation(
+      url_b, blink::mojom::NavigationBlockedReason::kRedirectWithNoUserGesture);
+  EXPECT_EQ(1, delegate.blocked_navigation_count());
+}
+
 class RenderFrameHostImplPrerenderBrowserTest
     : public RenderFrameHostImplBrowserTest {
  public:
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.