Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Navigation
DescriptionInsufficient policy enforcement in Navigation
ComponentNavigation
Bug ClassLogic Error
Tracker497595264
Fix commit31e678d204cd (chromium/src) +84/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/render_frame_host_impl.cc
modified

Files Changed

  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/renderer_host/render_frame_host_impl_browsertest.cc
From 31e678d204cda637f180bb16f19288a74a06ebb9 Mon Sep 17 00:00:00 2001
From: Mike West <[email protected]>
Date: Tue, 07 Apr 2026 04:53:12 -0700
Subject: [PATCH] PendingNavigation should own the keep-alive handle.

Otherwise, edge cases can cause the initiator's policies to be lost
during deferred navigations, which can remove certain security-relevant
checks (CSP's `form-action`, for example).

Bug: 497595264
Change-Id: Icd91db362f25f6ce7f141ae04c75520c55ad60cc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7719744
Commit-Queue: Mike West <[email protected]>
Reviewed-by: Arthur Sonzogni <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1610667}
---

diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index ac474e22..3c55e866 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -2209,7 +2209,7 @@
   blink::mojom::CommonNavigationParamsPtr common_params_;
   blink::mojom::BeginNavigationParamsPtr begin_navigation_params_;
   mojo::Remote<blink::mojom::NavigationStateKeepAliveHandle>
-      opener_keep_alive_handle_;
+      initiator_navigation_state_keep_alive_handle_;
   scoped_refptr<network::SharedURLLoaderFactory> blob_url_loader_factory_;
   mojo::PendingAssociatedRemote<mojom::NavigationClient> navigation_client_;
   mojo::PendingReceiver<mojom::NavigationRendererCancellationListener>
@@ -2222,6 +2222,8 @@
       blink::mojom::BeginNavigationParamsPtr begin_navigation_params,
       scoped_refptr<network::SharedURLLoaderFactory> blob_url_loader_factory,
       mojo::PendingAssociatedRemote<mojom::NavigationClient> navigation_client,
+      mojo::PendingRemote<blink::mojom::NavigationStateKeepAliveHandle>
+          initiator_navigation_state_keep_alive_handle,
       mojo::PendingReceiver<mojom::NavigationRendererCancellationListener>
           renderer_cancellation_listener,
       mojo::PendingReceiver<
@@ -2235,6 +2237,8 @@
     blink::mojom::BeginNavigationParamsPtr begin_navigation_params,
     scoped_refptr<network::SharedURLLoaderFactory> blob_url_loader_factory,
     mojo::PendingAssociatedRemote<mojom::NavigationClient> navigation_client,
+    mojo::PendingRemote<blink::mojom::NavigationStateKeepAliveHandle>
+        initiator_navigation_state_keep_alive_handle,
     mojo::PendingReceiver<mojom::NavigationRendererCancellationListener>
         renderer_cancellation_listener,
     mojo::PendingReceiver<blink::mojom::NavigationResumeDeferredCommitListener>
@@ -2248,9 +2252,16 @@
           std::move(renderer_cancellation_listener)),
       deferred_commit_resume_listener_(
           std::move(deferred_commit_resume_listener)) {
-  if (initiator_frame) {
+  if (initiator_navigation_state_keep_alive_handle) {
+    initiator_navigation_state_keep_alive_handle_.Bind(
+        std::move(initiator_navigation_state_keep_alive_handle));
+  } else if (initiator_frame) {
+    // TODO(500074274): It would be ideal to drop this in favor of something
+    // like `CHECK(!initiator_frame ||
+    //             initiator_navigation_state_keep_alive_handle)`.
     initiator_frame->IssueKeepAliveHandle(
-        opener_keep_alive_handle_.BindNewPipeAndPassReceiver());
+        initiator_navigation_state_keep_alive_handle_
+            .BindNewPipeAndPassReceiver());
   }
 }
 
@@ -11653,6 +11664,7 @@
     pending_navigate_ = std::make_unique<PendingNavigation>(
         std::move(validated_common_params), std::move(begin_params),
         std::move(blob_url_loader_factory), std::move(navigation_client),
+        std::move(initiator_navigation_state_keep_alive_handle),
         std::move(renderer_cancellation_listener),
         std::move(deferred_commit_resume_listener), initiator_frame);
     return;
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 7c29e5a..b033c0df 100644
--- a/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
+++ b/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
@@ -10611,4 +10611,73 @@
   EXPECT_EQ(allowlist_url, web_contents()->GetLastCommittedURL());
 }
 
+IN_PROC_BROWSER_TEST_F(RenderFrameHostImplBrowserTest,
+                       DeferredPopupNavigationPreservesInitiatorPolicies) {
+  // 1. Force WebContents in a new Shell to defer new navigations until the
+  // delegate is set.
+  shell()->set_delay_popup_contents_delegate_for_testing(true);
+
+  // 2. Load a page with an iframe.
+  GURL url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), url));
+
+  // 3. Navigate the iframe to a page with CSP form-action 'none'.
+  GURL iframe_url = embedded_test_server()->GetURL(
+      "a.com", "/set-header?Content-Security-Policy: form-action 'none'");
+  EXPECT_TRUE(ExecJs(shell(), JsReplace(R"(
+    let iframe = document.createElement('iframe');
+    iframe.id = 'initiator_iframe';
+    iframe.src = $1;
+    document.body.appendChild(iframe);
+  )",
+                                        iframe_url)));
+  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+
+  RenderFrameHostImpl* iframe_rfh = static_cast<RenderFrameHostImpl*>(
+      ChildFrameAt(shell()->web_contents()->GetPrimaryMainFrame(), 0));
+  ASSERT_TRUE(iframe_rfh);
+
+  // 4. From the iframe, open a popup that will navigate to a same-site URL.
+  // The navigation should be deferred because of step 1.
+  GURL popup_url = embedded_test_server()->GetURL("a.com", "/title2.html");
+  ShellAddedObserver new_shell_observer;
+  EXPECT_TRUE(ExecJs(iframe_rfh, JsReplace("window.open($1);", popup_url)));
+  Shell* new_shell = new_shell_observer.GetShell();
+  WebContentsImpl* new_contents =
+      static_cast<WebContentsImpl*>(new_shell->web_contents());
+
+  // The navigation in the new popup should be deferred.
+  EXPECT_TRUE(WaitForLoadStop(new_contents));
+  EXPECT_TRUE(new_contents->GetController().IsInitialBlankNavigation());
+
+  // 5. Detach the initiator iframe.
+  EXPECT_TRUE(ExecJs(shell(), R"(
+    let iframe = document.getElementById('initiator_iframe');
+    iframe.remove();
+  )"));
+
+  // 6. Resume the deferred navigation.
+  new_contents->SetDelegate(new_shell);
+  new_contents->ResumeLoadingCreatedWebContents();
+
+  // 7. Verify that the navigation in the popup inherits the initiator's CSP.
+  // Since form-action is 'none', a form submission should be blocked.
+  EXPECT_TRUE(WaitForLoadStop(new_contents));
+  EXPECT_EQ(popup_url, new_contents->GetLastCommittedURL());
+
+  // Try to submit a form. It should be blocked by CSP if policies were
+  // preserved.
+  EXPECT_TRUE(ExecJs(new_contents, R"(
+    let form = document.createElement('form');
+    form.action = '/title3.html';
+    document.body.appendChild(form);
+    form.submit();
+  )"));
+
+  // If CSP form-action 'none' was preserved, the navigation to title3.html
+  // should not happen, and we should still be on popup_url (or about:blank if
+  // blocked early).
+  EXPECT_EQ(popup_url, new_contents->GetLastCommittedURL());
+}
+
 }  // namespace content
Loading diff…

Regression Test / PoC

shipped with the fix
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 7c29e5a..b033c0df 100644
--- a/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
+++ b/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
@@ -10611,4 +10611,73 @@
   EXPECT_EQ(allowlist_url, web_contents()->GetLastCommittedURL());
 }
 
+IN_PROC_BROWSER_TEST_F(RenderFrameHostImplBrowserTest,
+                       DeferredPopupNavigationPreservesInitiatorPolicies) {
+  // 1. Force WebContents in a new Shell to defer new navigations until the
+  // delegate is set.
+  shell()->set_delay_popup_contents_delegate_for_testing(true);
+
+  // 2. Load a page with an iframe.
+  GURL url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), url));
+
+  // 3. Navigate the iframe to a page with CSP form-action 'none'.
+  GURL iframe_url = embedded_test_server()->GetURL(
+      "a.com", "/set-header?Content-Security-Policy: form-action 'none'");
+  EXPECT_TRUE(ExecJs(shell(), JsReplace(R"(
+    let iframe = document.createElement('iframe');
+    iframe.id = 'initiator_iframe';
+    iframe.src = $1;
+    document.body.appendChild(iframe);
+  )",
+                                        iframe_url)));
+  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+
+  RenderFrameHostImpl* iframe_rfh = static_cast<RenderFrameHostImpl*>(
+      ChildFrameAt(shell()->web_contents()->GetPrimaryMainFrame(), 0));
+  ASSERT_TRUE(iframe_rfh);
+
+  // 4. From the iframe, open a popup that will navigate to a same-site URL.
+  // The navigation should be deferred because of step 1.
+  GURL popup_url = embedded_test_server()->GetURL("a.com", "/title2.html");
+  ShellAddedObserver new_shell_observer;
+  EXPECT_TRUE(ExecJs(iframe_rfh, JsReplace("window.open($1);", popup_url)));
+  Shell* new_shell = new_shell_observer.GetShell();
+  WebContentsImpl* new_contents =
+      static_cast<WebContentsImpl*>(new_shell->web_contents());
+
+  // The navigation in the new popup should be deferred.
+  EXPECT_TRUE(WaitForLoadStop(new_contents));
+  EXPECT_TRUE(new_contents->GetController().IsInitialBlankNavigation());
+
+  // 5. Detach the initiator iframe.
+  EXPECT_TRUE(ExecJs(shell(), R"(
+    let iframe = document.getElementById('initiator_iframe');
+    iframe.remove();
+  )"));
+
+  // 6. Resume the deferred navigation.
+  new_contents->SetDelegate(new_shell);
+  new_contents->ResumeLoadingCreatedWebContents();
+
+  // 7. Verify that the navigation in the popup inherits the initiator's CSP.
+  // Since form-action is 'none', a form submission should be blocked.
+  EXPECT_TRUE(WaitForLoadStop(new_contents));
+  EXPECT_EQ(popup_url, new_contents->GetLastCommittedURL());
+
+  // Try to submit a form. It should be blocked by CSP if policies were
+  // preserved.
+  EXPECT_TRUE(ExecJs(new_contents, R"(
+    let form = document.createElement('form');
+    form.action = '/title3.html';
+    document.body.appendChild(form);
+    form.submit();
+  )"));
+
+  // If CSP form-action 'none' was preserved, the navigation to title3.html
+  // should not happen, and we should still be on popup_url (or about:blank if
+  // blocked early).
+  EXPECT_EQ(popup_url, new_contents->GetLastCommittedURL());
+}
+
 }  // namespace content
Loading diff…

Original Bug Report

reported by [email protected]

CSP and LNA bypass via dropped keep-alive handle in deferred popup navigations

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A logic flaw in RenderFrameHostImpl::BeginNavigation drops the initiator’s security policy keep-alive handle during deferred navigations, such as Android WebView popups. If the attacker immediately destroys the initiator frame, its policies are permanently lost because the fallback keep-alive mechanism fails. This results in a potential bypass of Content Security Policy (CSP) form-action checks and Local Network Access (LNA) restrictions when the navigation eventually resumes.

Affected files:

  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/renderer_host/navigation_request.cc
  • content/browser/renderer_host/navigation_policy_container_builder.cc

Estimated timestamp from git blame: 2026-01-29

Description

There is a potential vulnerability in how Chrome handles initiator security policies for deferred popup navigations. When an embedder defers the creation of a new window (e.g., Android WebView returning false from ShouldResumeRequestsForCreatedWindow), the browser sets waiting_for_init_ to true for the new RenderFrameHostImpl.

When the renderer initiates a navigation for this deferred window, it sends an IPC to RenderFrameHostImpl::BeginNavigation. Crucially, this IPC includes an initiator_navigation_state_keep_alive_handle designed to keep the initiator’s PolicyContainerHost alive even if the initiator frame is subsequently destroyed.

However, in RenderFrameHostImpl::BeginNavigation, if waiting_for_init_ is true, the code stores the navigation parameters in a PendingNavigation object and returns early. The initiator_navigation_state_keep_alive_handle parameter is dropped.

The PendingNavigation constructor attempts to compensate for this by synchronously issuing a new keep-alive handle: initiator_frame->IssueKeepAliveHandle(...). This relies on resolving the initiator_frame via its frame token. If an attacker triggers the navigation and immediately removes the initiator frame from the DOM, the browser may process the frame detachment before or concurrently with the BeginNavigation IPC. In this scenario, initiator_frame is nullptr, the fallback fails silently, and the PolicyContainerHost is garbage collected.

Impact

When the embedder eventually resumes the deferred window (via ResumeLoadingCreatedWebContents), the NavigationRequest is created but cannot find the initiator’s policies. Because NavigationPolicyContainerBuilder::GetInitiatorPolicies() returns null, two major security mechanisms are bypassed:

  1. Local Network Access (LNA) Bypass: NavigationRequest::BuildClientSecurityStateForNavigationFetch() returns nullptr, meaning the network request proceeds without a ClientSecurityState. The Network Service fails to enforce LNA preflight checks for the cross-origin request.
  2. CSP form-action Bypass: NavigationRequest::CheckCSPDirectives() skips the form-action check entirely because the initiator_policies object is null.

Potential Reproduction Steps

Note: These are suggested steps based on static code analysis. Our tooling agent does not currently have the ability to run code or verify the exploit dynamically.

  1. An attacker hosts a page on a public IP and a victim loads it in an Android WebView app that supports multiple windows.
  2. The attacker’s page creates an iframe.
  3. Inside the iframe, an attacker script dynamically creates a <form> with target="_blank" and action set to a local network resource (e.g., http://192.168.1.1/admin).
  4. The script calls form.submit() and immediately calls iframe.remove() to detach the initiator frame.
  5. The browser defers the popup navigation (waiting_for_init_ is true) and drops the keep-alive handle. Because the frame was detached, the fallback keep-alive handle creation fails.
  6. When the application resumes the popup, the POST request is sent to the local network device without LNA preflight checks or CSP form-action enforcement, achieving a CSRF attack.

Suggested Fix

The PendingNavigation struct should be updated to take and store the original mojo::PendingRemote<blink::mojom::NavigationStateKeepAliveHandle> passed into RenderFrameHostImpl::BeginNavigation, rather than discarding it.

By transferring ownership of the provided handle into PendingNavigation, the policy container host will remain alive as long as the deferred navigation is pending, regardless of whether the initiator_frame is destroyed in the meantime.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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.

View on issue tracker
Links in the report