CVE-2026-113281
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/render_frame_host_impl.cc |
modified |
Files Changed
content/browser/download/download_browsertest.cccontent/browser/renderer_host/render_frame_host_impl.cctools/metrics/histograms/enums.xmlcontent/browser/security_exploit_browsertest.cccontent/public/browser/disallow_activation_reason.h
Patch
From fe552b9103581d742bc057094c9c95a717dda645 Mon Sep 17 00:00:00 2001
From: Rakina Zata Amni <rakina@chromium.org>
Date: Tue, 15 Sep 2026 06:34:20 -0700
Subject: [PATCH] Apply lifecycle checks earlier in RFHI DownloadURL and OpenURL
RenderFrameHostImpl::DownloadURL previously relied on
DownloadManagerImpl to reject requests from inactive or fenced
frames, but that gate is skipped for requests marked as
`is_context_menu_save` (which maps to `!content_initiated`).
Apply the IsInactiveAndDisallowActivation and
IsNestedWithinFencedFrame checks in DownloadURL itself so they
run regardless of any renderer-supplied parameters.
Also change the existing `!owner_` early return in
RenderFrameHostImpl::OpenURL to be an
IsInactiveAndDisallowActivation and move it earlier to above
the `_unfencedTop` handling so inactive documents are rejected
on that path as well.
Add DisallowActivationReasonId::kDownloadUrl for the new check
and update enums.xml.
Update DownloadFencedFrameTest.DiscardNonNavigationDownload to
reflect that the request no longer reaches the download
manager, and add regression tests covering
`is_context_menu_save` from a fenced frame and from a
pending-deletion frame.
TAG=agy
CONV=26f92c15-d707-47f0-8207-e315be2a395d
Fixed: 498742883
Change-Id: Id94d2ea7c0f1bba4f785514a63d570dede4b72d0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8174165
Commit-Queue: Rakina Zata Amni <rakina@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1698026}
---
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index d63fe6f3..38c4cec 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -7990,6 +7990,15 @@
return;
}
+ // Don't start a download if it's triggered on a non-active RenderFrameHost
+ // or is contained in a fenced frame, regardless of any renderer-supplied
+ // parameters such as `is_context_menu_save`.
+ if (IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kDownloadUrl) ||
+ IsNestedWithinFencedFrame()) {
+ return;
+ }
+
if (!VerifyDownloadUrlParams(GetProcess(), *blink_parameters)) {
return;
}
@@ -10323,6 +10332,17 @@
return;
}
+ // Only active and prerendered documents are allowed to start navigation in
+ // their frame.
+ if (lifecycle_state() != LifecycleStateImpl::kPrerendering) {
+ // If this is reached in case the RenderFrameHost is in BackForwardCache
+ // evict the document from BackForwardCache.
+ if (IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kBeginNavigation)) {
+ return;
+ }
+ }
+
// If the flag `is_unfenced_top_navigation` is set, this is a special code
// path for MPArch fenced frames. The target frame doesn't have a handle
// inside the MPArch renderer process, so we need to set it here.
@@ -10423,16 +10443,10 @@
}
}
- RenderFrameHostOwner* owner = owner_;
- // Inactive documents are not allowed to initiate navigations.
- // Also, see a similar check in RenderFrameHostImpl::BeginNavigation at
- // https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/render_frame_host_impl.cc;l=7761-7769;drc=6dc39d60fea45c003424272efdb4c366119a9d7f
- if (!owner) {
- return;
- }
// TODO(crbug.com/385170155): Consider whether to pass actual_navigation_start
// through this path as well, which may involve a lot of plumbing.
- owner->GetCurrentNavigator().RequestOpenURL(
+ CHECK(owner_);
+ owner_->GetCurrentNavigator().RequestOpenURL(
this, validated_url, base::OptionalToPtr(params->initiator_frame_token),
GetProcess()->GetDeprecatedID(), params->initiator_origin,
params->initiator_base_url, initiator_navigation_state, params->post_body,
diff --git a/content/public/browser/disallow_activation_reason.h b/content/public/browser/disallow_activation_reason.h
index 21a9608..5d51c0f 100644
--- a/content/public/browser/disallow_activation_reason.h
+++ b/content/public/browser/disallow_activation_reason.h
@@ -71,6 +71,7 @@
kClipboard = 47,
kShowCaptionSettings = 48,
kPictureInPictureService = 49,
+ kDownloadUrl = 50,
// New entries go above here. New entries should be added to
// tools/metrics/histograms/enums.xml .
kMinEmbedderDisallowActivationReason = 2 << 16,
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index a08b19b4..a91b6a0 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -5952,6 +5952,9 @@
A PictureInPictureService IPC arrived for a frame in BFCache or pending
deletion.
</int>
+ <int value="50" label="kDownloadUrl">
+ A DownloadURL IPC arrived for an inactive frame.
+ </int>
</enum>
<enum name="IncidentType">
diff --git a/content/browser/download/download_browsertest.cc b/content/browser/download/download_browsertest.cc
index 9e57315f..9295ccde 100644
--- a/content/browser/download/download_browsertest.cc
+++ b/content/browser/download/download_browsertest.cc
@@ -58,6 +58,7 @@
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/common/webplugininfo.h"
+#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
@@ -66,6 +67,7 @@
#include "content/public/test/download_test_observer.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/prerender_test_util.h"
+#include "content/public/test/render_frame_host_test_support.h"
#include "content/public/test/slow_download_http_response.h"
#include "content/public/test/test_download_http_response.h"
#include "content/public/test/test_file_error_injector.h"
@@ -5476,12 +5478,12 @@
shell()->web_contents()->GetPrimaryMainFrame(), kFencedFrameUrl);
// Do a download without navigation from the fenced frame RenderFrameHost.
- // The download will be dropped.
+ // The download should not reach the download manager.
auto* download_manager =
fenced_frame_host->GetBrowserContext()->GetDownloadManager();
MockDownloadManagerObserver dm_observer(download_manager);
EXPECT_CALL(dm_observer, OnDownloadCreated(_, _)).Times(0);
- EXPECT_CALL(dm_observer, OnDownloadDropped(_)).Times(1);
+ EXPECT_CALL(dm_observer, OnDownloadDropped(_)).Times(0);
auto params = blink::mojom::DownloadURLParams::New();
params->url = kDownloadUrl;
@@ -5511,12 +5513,12 @@
shell()->web_contents()->GetPrimaryMainFrame(), kFencedFrameUrl);
// Do a context-menu-save download from the fenced frame RenderFrameHost.
- // The download will be dropped.
+ // The download should not reach the download manager.
auto* download_manager =
fenced_frame_host->GetBrowserContext()->GetDownloadManager();
MockDownloadManagerObserver dm_observer(download_manager);
EXPECT_CALL(dm_observer, OnDownloadCreated(_, _)).Times(0);
- EXPECT_CALL(dm_observer, OnDownloadDropped(_)).Times(1);
+ EXPECT_CALL(dm_observer, OnDownloadDropped(_)).Times(0);
auto params = blink::mojom::DownloadURLParams::New();
params->url = kDownloadUrl;
@@ -5531,6 +5533,57 @@
EXPECT_TRUE(downloads.empty());
}
+// Verify that a DownloadURL request with `is_context_menu_save` set is
+// discarded when initiated from a frame that is pending deletion.
+IN_PROC_BROWSER_TEST_F(DownloadContentTest,
+ DiscardDownloadFromPendingDeletionFrame) {
+ const GURL kUrlA = embedded_test_server()->GetURL("a.test", "/empty.html");
+ const GURL kUrlB = embedded_test_server()->GetURL("b.test", "/empty.html");
+ const GURL kDownloadUrl =
+ embedded_test_server()->GetURL("a.test", "/download/download-test.lib");
+
+ DisableBackForwardCacheForTesting(shell()->web_contents(),
+ BackForwardCache::TEST_REQUIRES_NO_CACHING);
+
+ // Navigate to A and force the RenderFrameHost to linger after being
+ // navigated away from.
+ EXPECT_TRUE(NavigateToURL(shell(), kUrlA));
+ auto* rfh_a = static_cast<RenderFrameHostImpl*>(
+ shell()->web_contents()->GetPrimaryMainFrame());
+ LeaveInPendingDeletionState(rfh_a);
+
+ // Navigate to B so that `rfh_a` enters the pending deletion state.
+ EXPECT_TRUE(NavigateToURL(shell(), kUrlB));
+ EXPECT_FALSE(rfh_a->IsActive());
+
+ auto* download_manager = DownloadManagerForShell(shell());
+ MockDownloadManagerObserver dm_observer(download_manager);
+ EXPECT_CALL(dm_observer, OnDownloadCreated(_, _)).Times(1);
+ EXPECT_CALL(dm_observer, OnDownloadDropped(_)).Times(0);
+
+ // Send a DownloadURL request from the pending deletion frame with
+ // `is_context_menu_save` set. It should not result in a download.
+ auto params = blink::mojom::DownloadURLParams::New();
+ params->url = kDownloadUrl;
+ params->should_prompt_for_save_location = true;
+ rfh_a->DownloadURL(std::move(params));
+
+ // Trigger a download from the primary main frame and wait for it to
+ // complete. This ensures any download from the request above would have
+ // been observed by now.
+ std::unique_ptr<DownloadTestObserver> observer(CreateWaiter(shell(), 1));
+ auto barrier_parameters =
+ DownloadRequestUtils::CreateDownloadForWebContentsMainFrame(
+ shell()->web_contents(), kDownloadUrl, TRAFFIC_ANNOTATION_FOR_TESTS);
+ download_manager->DownloadUrl(std::move(barrier_parameters));
+ observer->WaitForFinished();
+
+ // Verify only the primary main frame download exists.
+ std::vector<raw_ptr<download::DownloadItem, VectorExperimental>> downloads;
+ download_manager->GetAllDownloads(&downloads);
+ EXPECT_EQ(1u, downloads.size());
+}
+
// A download triggered by clicking on a link with a |download| attribute should
// have the user-gesture flag set.
IN_PROC_BROWSER_TEST_F(DownloadContentTest,
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index 734f718..49654e7 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -73,6 +73,7 @@
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/navigation_handle_observer.h"
+#include "content/public/test/render_frame_host_test_support.h"
#include "content/public/test/test_content_browser_client.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
@@ -4795,6 +4796,42 @@
kill_waiter.Wait());
}
+// Verify that an OpenURL request for an unfenced top navigation is ignored
+// when initiated from a frame that is pending deletion.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames,
+ UnfencedTopNavigationFromPendingDeletionFrame) {
+ ChildProcessSecurityPolicyImpl::GetInstance()->AddFutureIsolatedOrigins(
+ "https://b.test", ChildProcessSecurityPolicy::IsolatedOriginSource::TEST);
+
+ GURL main_url(https_server()->GetURL("a.test", "/simple_page.html"));
+ EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+ 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);
+ EXPECT_TRUE(fenced_rfh_generic);
+ RenderFrameHostImpl* fenced_rfh =
+ static_cast<RenderFrameHostImpl*>(fenced_rfh_generic);
+
+ LeaveInPendingDeletionState(fenced_rfh);
+ GURL fenced_url2(
+ https_server()->GetURL("b.test", "/fenced_frames/title1.html"));
+ fenced_frame_test_helper().NavigateFrameInFencedFrameTree(fenced_rfh,
+ fenced_url2);
+ EXPECT_FALSE(fenced_rfh->IsActive());
+
+ GURL dest_url(https_server()->GetURL("b.test", "/simple_page.html"));
+ TestNavigationManager nav_manager(shell()->web_contents(), dest_url);
+
+ auto params = CreateOpenURLParams(dest_url, fenced_rfh);
+ params->is_unfenced_top_navigation = true;
+ static_cast<mojom::FrameHost*>(fenced_rfh)->OpenURL(std::move(params));
+
+ EXPECT_FALSE(nav_manager.GetNavigationHandle());
+}
+
namespace {
// Interceptor that replaces the origin in the DidCommitProvisionalLoadParams
Regression Test / PoC
diff --git a/content/browser/download/download_browsertest.cc b/content/browser/download/download_browsertest.cc
index 9e57315f..9295ccde 100644
--- a/content/browser/download/download_browsertest.cc
+++ b/content/browser/download/download_browsertest.cc
@@ -58,6 +58,7 @@
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/common/webplugininfo.h"
+#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
@@ -66,6 +67,7 @@
#include "content/public/test/download_test_observer.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/prerender_test_util.h"
+#include "content/public/test/render_frame_host_test_support.h"
#include "content/public/test/slow_download_http_response.h"
#include "content/public/test/test_download_http_response.h"
#include "content/public/test/test_file_error_injector.h"
@@ -5476,12 +5478,12 @@
shell()->web_contents()->GetPrimaryMainFrame(), kFencedFrameUrl);
// Do a download without navigation from the fenced frame RenderFrameHost.
- // The download will be dropped.
+ // The download should not reach the download manager.
auto* download_manager =
fenced_frame_host->GetBrowserContext()->GetDownloadManager();
MockDownloadManagerObserver dm_observer(download_manager);
EXPECT_CALL(dm_observer, OnDownloadCreated(_, _)).Times(0);
- EXPECT_CALL(dm_observer, OnDownloadDropped(_)).Times(1);
+ EXPECT_CALL(dm_observer, OnDownloadDropped(_)).Times(0);
auto params = blink::mojom::DownloadURLParams::New();
params->url = kDownloadUrl;
@@ -5511,12 +5513,12 @@
shell()->web_contents()->GetPrimaryMainFrame(), kFencedFrameUrl);
// Do a context-menu-save download from the fenced frame RenderFrameHost.
- // The download will be dropped.
+ // The download should not reach the download manager.
auto* download_manager =
fenced_frame_host->GetBrowserContext()->GetDownloadManager();
MockDownloadManagerObserver dm_observer(download_manager);
EXPECT_CALL(dm_observer, OnDownloadCreated(_, _)).Times(0);
- EXPECT_CALL(dm_observer, OnDownloadDropped(_)).Times(1);
+ EXPECT_CALL(dm_observer, OnDownloadDropped(_)).Times(0);
auto params = blink::mojom::DownloadURLParams::New();
params->url = kDownloadUrl;
@@ -5531,6 +5533,57 @@
EXPECT_TRUE(downloads.empty());
}
+// Verify that a DownloadURL request with `is_context_menu_save` set is
+// discarded when initiated from a frame that is pending deletion.
+IN_PROC_BROWSER_TEST_F(DownloadContentTest,
+ DiscardDownloadFromPendingDeletionFrame) {
+ const GURL kUrlA = embedded_test_server()->GetURL("a.test", "/empty.html");
+ const GURL kUrlB = embedded_test_server()->GetURL("b.test", "/empty.html");
+ const GURL kDownloadUrl =
+ embedded_test_server()->GetURL("a.test", "/download/download-test.lib");
+
+ DisableBackForwardCacheForTesting(shell()->web_contents(),
+ BackForwardCache::TEST_REQUIRES_NO_CACHING);
+
+ // Navigate to A and force the RenderFrameHost to linger after being
+ // navigated away from.
+ EXPECT_TRUE(NavigateToURL(shell(), kUrlA));
+ auto* rfh_a = static_cast<RenderFrameHostImpl*>(
+ shell()->web_contents()->GetPrimaryMainFrame());
+ LeaveInPendingDeletionState(rfh_a);
+
+ // Navigate to B so that `rfh_a` enters the pending deletion state.
+ EXPECT_TRUE(NavigateToURL(shell(), kUrlB));
+ EXPECT_FALSE(rfh_a->IsActive());
+
+ auto* download_manager = DownloadManagerForShell(shell());
+ MockDownloadManagerObserver dm_observer(download_manager);
+ EXPECT_CALL(dm_observer, OnDownloadCreated(_, _)).Times(1);
+ EXPECT_CALL(dm_observer, OnDownloadDropped(_)).Times(0);
+
+ // Send a DownloadURL request from the pending deletion frame with
+ // `is_context_menu_save` set. It should not result in a download.
+ auto params = blink::mojom::DownloadURLParams::New();
+ params->url = kDownloadUrl;
+ params->should_prompt_for_save_location = true;
+ rfh_a->DownloadURL(std::move(params));
+
+ // Trigger a download from the primary main frame and wait for it to
+ // complete. This ensures any download from the request above would have
+ // been observed by now.
+ std::unique_ptr<DownloadTestObserver> observer(CreateWaiter(shell(), 1));
+ auto barrier_parameters =
+ DownloadRequestUtils::CreateDownloadForWebContentsMainFrame(
+ shell()->web_contents(), kDownloadUrl, TRAFFIC_ANNOTATION_FOR_TESTS);
+ download_manager->DownloadUrl(std::move(barrier_parameters));
+ observer->WaitForFinished();
+
+ // Verify only the primary main frame download exists.
+ std::vector<raw_ptr<download::DownloadItem, VectorExperimental>> downloads;
+ download_manager->GetAllDownloads(&downloads);
+ EXPECT_EQ(1u, downloads.size());
+}
+
// A download triggered by clicking on a link with a |download| attribute should
// have the user-gesture flag set.
IN_PROC_BROWSER_TEST_F(DownloadContentTest,
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index 734f718..49654e7 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -73,6 +73,7 @@
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/navigation_handle_observer.h"
+#include "content/public/test/render_frame_host_test_support.h"
#include "content/public/test/test_content_browser_client.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
@@ -4795,6 +4796,42 @@
kill_waiter.Wait());
}
+// Verify that an OpenURL request for an unfenced top navigation is ignored
+// when initiated from a frame that is pending deletion.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames,
+ UnfencedTopNavigationFromPendingDeletionFrame) {
+ ChildProcessSecurityPolicyImpl::GetInstance()->AddFutureIsolatedOrigins(
+ "https://b.test", ChildProcessSecurityPolicy::IsolatedOriginSource::TEST);
+
+ GURL main_url(https_server()->GetURL("a.test", "/simple_page.html"));
+ EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+ 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);
+ EXPECT_TRUE(fenced_rfh_generic);
+ RenderFrameHostImpl* fenced_rfh =
+ static_cast<RenderFrameHostImpl*>(fenced_rfh_generic);
+
+ LeaveInPendingDeletionState(fenced_rfh);
+ GURL fenced_url2(
+ https_server()->GetURL("b.test", "/fenced_frames/title1.html"));
+ fenced_frame_test_helper().NavigateFrameInFencedFrameTree(fenced_rfh,
+ fenced_url2);
+ EXPECT_FALSE(fenced_rfh->IsActive());
+
+ GURL dest_url(https_server()->GetURL("b.test", "/simple_page.html"));
+ TestNavigationManager nav_manager(shell()->web_contents(), dest_url);
+
+ auto params = CreateOpenURLParams(dest_url, fenced_rfh);
+ params->is_unfenced_top_navigation = true;
+ static_cast<mojom::FrameHost*>(fenced_rfh)->OpenURL(std::move(params));
+
+ EXPECT_FALSE(nav_manager.GetNavigationHandle());
+}
+
namespace {
// Interceptor that replaces the origin in the DidCommitProvisionalLoadParams
Original Bug Report
Missing lifecycle checks in DownloadURL and OpenURL allow spoofing and hijacking
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 without the security team.
Overview: Two potential missing lifecycle checks in RenderFrameHostImpl allow a compromised renderer to perform actions from inactive or dying frames. In DownloadURL, forging the is_context_menu_save flag bypasses inactivity checks in the download manager, enabling dying frames to trigger UI prompts. In OpenURL, a fenced frame navigation path returns early before verifying frame activity, potentially allowing a dying subframe to hijack top-level navigations.
Affected files:
content/browser/renderer_host/render_frame_host_impl.cc
Estimated timestamp from git blame: 2026-01-28
We have identified two potential vulnerabilities in content/browser/renderer_host/render_frame_host_impl.cc where Mojo IPC handlers do not adequately verify if a frame is in an active lifecycle state before processing requests. This allows a compromised renderer to exploit frames in the process of unloading (e.g., kRunningUnloadHandlers) or pending deletion.
Note: The steps below are suggested/potential theoretical steps to trigger the vulnerabilities, as our tooling agent does not yet have the ability to run code and provide a working proof of concept.
1. DownloadURL Lifecycle Bypass (UI Spoofing)
In RenderFrameHostImpl::DownloadURL, the code converts blink_parameters->is_context_menu_save into content_initiated = false on the resulting DownloadUrlParameters. When this is passed down to DownloadManagerImpl::BeginDownloadInternal, critical lifecycle checks (rfh->IsInactiveAndDisallowActivation(...) and rfh->IsNestedWithinFencedFrame()) are entirely bypassed because they are gated behind an if (rfh && content_initiated) check.
Potential Attacker Steps:
- Attacker compromises a renderer process (e.g., via a v8 bug).
- The attacker triggers the unloading of a subframe they control.
- While the frame is in an inactive state (e.g.,
kRunningUnloadHandlers), the compromised renderer sends aLocalFrameHost::DownloadURLMojo message. - The payload forges
is_context_menu_savetotrue. - The browser processes the request, bypassing the
DownloadManagerImplinactivity checks, and incorrectly displays a “Save As” prompt. This can be used for UI spoofing from a stale or hidden context.
2. OpenURL Lifecycle Bypass (Fenced Frame Hijacking)
In RenderFrameHostImpl::OpenURL, there is a special code block to handle Fenced Frame navigations to _unfencedTop (triggered when params->is_unfenced_top_navigation is true). This block validates the request, navigates the outermost main frame, and then executes an early return;. Because it returns early, it completely bypasses the standard if (!owner_) { return; } lifecycle check located further down in the function, which normally prevents inactive documents from initiating navigations.
Potential Attacker Steps:
- Attacker compromises a renderer process that contains a Fenced Frame.
- The attacker triggers the unloading of the Fenced Frame, setting its
owner_to null. - The compromised renderer sends a
LocalFrameHost::OpenURLMojo message for the dying frame. - The payload sets
is_unfenced_top_navigation = trueand forgesuser_gesture = true. - The browser processes the
_unfencedTopnavigation and navigates the outermost main frame before reaching theowner_check. This allows a dying Fenced Frame to hijack the top-level page, bypassing intended lifecycle and privacy restrictions.
Suggested Fix
Insert an early frame activity check at the top of both RenderFrameHostImpl::DownloadURL and RenderFrameHostImpl::OpenURL.
For example, immediately after parameter unpacking, add:
if (IsInactiveAndDisallowActivation(DisallowActivationReasonId::k...)) {
return;
}
This ensures that inactive frames are consistently blocked before any side effects or early returns can occur.
Evaluated with Chrome root at commit: a79fb35f774128179732608bb342f988ebe71378
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.