Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Navigation
DescriptionInsufficient validation of untrusted input in Navigation
ComponentNavigation
Bug ClassLogic Error
Tracker497365545
Fix commit59cf8131947d (chromium/src) +81/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
ClientSideRedirectUrlReplacer
content/browser/security_exploit_browsertest.cc
modified
if
content/browser/security_exploit_browsertest.cc
modified
RemoteFrameHostInterceptor
content/browser/security_exploit_browsertest.cc
modified

Files Changed

  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/security_exploit_browsertest.cc
From 59cf8131947d105468cfb47814a8c1ead94f72c3 Mon Sep 17 00:00:00 2001
From: Alex Moshchuk <[email protected]>
Date: Tue, 31 Mar 2026 00:14:48 -0700
Subject: [PATCH] Filter client_side_redirect_url in BeginNavigation IPC

Unlike other navigation parameters (e.g., common_params.url,
searchable_form_url), the client_side_redirect_url in
blink::mojom::BeginNavigationParams was passed through
RenderFrameHostImpl::BeginNavigation without being validated or
filtered (e.g., by RenderProcessHost::FilterURL). This CL fixes that.

Fixed: 497365545
Change-Id: Ia0c6ad1bb493789657c4a4ec734f90cfb43805da
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7713438
Commit-Queue: Alex Moshchuk <[email protected]>
Reviewed-by: Charlie Reis <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1607681}
---

diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index c5646213..c402f90 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -11565,7 +11565,9 @@
     }
   }
 
+  // TODO(crbug.com/40066983): Consider converting these into renderer kills.
   GetProcess()->FilterURL(true, &begin_params->searchable_form_url);
+  GetProcess()->FilterURL(true, &begin_params->client_side_redirect_url);
 
   // If the request was for a blob URL, but the validated URL is no longer a
   // blob URL, reset the blob_url_token to prevent hitting the ReportBadMessage
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index b500c32..a280237d 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -1030,6 +1030,85 @@
             child->current_frame_host()->GetLastCommittedURL());
 }
 
+// Helper class to replace a particular URL as the client_side_redirect_url
+// in BeginNavigationParams.
+class ClientSideRedirectUrlReplacer : public FrameHostInterceptor {
+ public:
+  ClientSideRedirectUrlReplacer(WebContents* web_contents,
+                                const GURL& url_to_inject)
+      : FrameHostInterceptor(web_contents), url_to_inject_(url_to_inject) {}
+
+  ClientSideRedirectUrlReplacer(const ClientSideRedirectUrlReplacer&) = delete;
+  ClientSideRedirectUrlReplacer& operator=(
+      const ClientSideRedirectUrlReplacer&) = delete;
+
+  bool WillDispatchBeginNavigation(
+      RenderFrameHost* render_frame_host,
+      blink::mojom::CommonNavigationParamsPtr* common_params,
+      blink::mojom::BeginNavigationParamsPtr* begin_params,
+      mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token,
+      mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client)
+      override {
+    if (is_activated_) {
+      (*begin_params)->client_side_redirect_url = url_to_inject_;
+      is_activated_ = false;
+    }
+    return true;
+  }
+
+  void Activate() { is_activated_ = true; }
+
+ private:
+  GURL url_to_inject_;
+  bool is_activated_ = false;
+};
+
+// Verify that a compromised renderer can't poison client_side_redirect_url
+// with a privileged URL, which could then be loaded via a subsequent
+// "request desktop site" operation.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+                       BlockIllegalClientSideRedirectUrl) {
+  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+  WebContentsImpl* web_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+
+  GURL webui_url(GetWebUIURL(kChromeUIGpuHost));
+  ClientSideRedirectUrlReplacer injector(web_contents, webui_url);
+
+  // Setup the interceptor to inject the WebUI URL into the next
+  // BeginNavigation's client_side_redirect_url.
+  injector.Activate();
+
+  // Trigger a normal renderer-initiated navigation to a benign URL. The
+  // injector will poison the client_side_redirect_url in that IPC's
+  // BeginNavigationParams.
+  GURL next_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
+  TestNavigationManager nav_manager(web_contents, next_url);
+  EXPECT_TRUE(ExecJs(web_contents, JsReplace("location.href = $1;", next_url)));
+
+  // Wait for the navigation to finish.
+  ASSERT_TRUE(nav_manager.WaitForNavigationFinished());
+  EXPECT_TRUE(nav_manager.was_successful());
+
+  // At this point, before the fix, the NavigationEntry has saved
+  // the WebUI URL as the OriginalRequestURL.
+
+  // Simulate the user clicking "Request Desktop Site" or similar,
+  // triggering a reload of the original request URL.
+  TestNavigationObserver reload_observer(web_contents);
+  web_contents->GetController().LoadOriginalRequestURL();
+  reload_observer.Wait();
+
+  // Ensure that the browser doesn't navigate to the WebUI URL, which should've
+  // been filtered out when processing the corresponding BeginNavigation IPC.
+  // TODO(crbug.com/40066983): Consider terminating the renderer process
+  // instead.
+  EXPECT_NE(webui_url, web_contents->GetLastCommittedURL());
+  EXPECT_EQ(GURL(kBlockedURL), web_contents->GetLastCommittedURL());
+}
+
 class RemoteFrameHostInterceptor
     : public blink::mojom::RemoteFrameHostInterceptorForTesting {
  public:
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 b500c32..a280237d 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -1030,6 +1030,85 @@
             child->current_frame_host()->GetLastCommittedURL());
 }
 
+// Helper class to replace a particular URL as the client_side_redirect_url
+// in BeginNavigationParams.
+class ClientSideRedirectUrlReplacer : public FrameHostInterceptor {
+ public:
+  ClientSideRedirectUrlReplacer(WebContents* web_contents,
+                                const GURL& url_to_inject)
+      : FrameHostInterceptor(web_contents), url_to_inject_(url_to_inject) {}
+
+  ClientSideRedirectUrlReplacer(const ClientSideRedirectUrlReplacer&) = delete;
+  ClientSideRedirectUrlReplacer& operator=(
+      const ClientSideRedirectUrlReplacer&) = delete;
+
+  bool WillDispatchBeginNavigation(
+      RenderFrameHost* render_frame_host,
+      blink::mojom::CommonNavigationParamsPtr* common_params,
+      blink::mojom::BeginNavigationParamsPtr* begin_params,
+      mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token,
+      mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client)
+      override {
+    if (is_activated_) {
+      (*begin_params)->client_side_redirect_url = url_to_inject_;
+      is_activated_ = false;
+    }
+    return true;
+  }
+
+  void Activate() { is_activated_ = true; }
+
+ private:
+  GURL url_to_inject_;
+  bool is_activated_ = false;
+};
+
+// Verify that a compromised renderer can't poison client_side_redirect_url
+// with a privileged URL, which could then be loaded via a subsequent
+// "request desktop site" operation.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+                       BlockIllegalClientSideRedirectUrl) {
+  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+  WebContentsImpl* web_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+
+  GURL webui_url(GetWebUIURL(kChromeUIGpuHost));
+  ClientSideRedirectUrlReplacer injector(web_contents, webui_url);
+
+  // Setup the interceptor to inject the WebUI URL into the next
+  // BeginNavigation's client_side_redirect_url.
+  injector.Activate();
+
+  // Trigger a normal renderer-initiated navigation to a benign URL. The
+  // injector will poison the client_side_redirect_url in that IPC's
+  // BeginNavigationParams.
+  GURL next_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
+  TestNavigationManager nav_manager(web_contents, next_url);
+  EXPECT_TRUE(ExecJs(web_contents, JsReplace("location.href = $1;", next_url)));
+
+  // Wait for the navigation to finish.
+  ASSERT_TRUE(nav_manager.WaitForNavigationFinished());
+  EXPECT_TRUE(nav_manager.was_successful());
+
+  // At this point, before the fix, the NavigationEntry has saved
+  // the WebUI URL as the OriginalRequestURL.
+
+  // Simulate the user clicking "Request Desktop Site" or similar,
+  // triggering a reload of the original request URL.
+  TestNavigationObserver reload_observer(web_contents);
+  web_contents->GetController().LoadOriginalRequestURL();
+  reload_observer.Wait();
+
+  // Ensure that the browser doesn't navigate to the WebUI URL, which should've
+  // been filtered out when processing the corresponding BeginNavigation IPC.
+  // TODO(crbug.com/40066983): Consider terminating the renderer process
+  // instead.
+  EXPECT_NE(webui_url, web_contents->GetLastCommittedURL());
+  EXPECT_EQ(GURL(kBlockedURL), web_contents->GetLastCommittedURL());
+}
+
 class RemoteFrameHostInterceptor
     : public blink::mojom::RemoteFrameHostInterceptorForTesting {
  public:
Loading diff…

Original Bug Report

reported by [email protected]

Renderer navigation bypass to privileged schemes via client_side_redirect_url

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

Overview: A compromised renderer can bypass security restrictions and navigate to privileged schemes like chrome:// or file:// by supplying a malicious client_side_redirect_url during navigation. When triggered by common browser UI actions like toggling “Request Desktop Site”, the browser reloads this poisoned URL as a fully trusted, browser-initiated navigation.

Affected files:

  • content/browser/renderer_host/navigation_controller_impl.cc
  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/renderer_host/ipc_utils.cc
  • content/browser/renderer_host/navigation_request.cc

Estimated timestamp from git blame: 2024-01-31

Description

There is a potential vulnerability in Chrome’s navigation handling where a compromised renderer can force the browser to navigate to privileged schemes (e.g., chrome://, file://, devtools://). This bypasses ChildProcessSecurityPolicy restrictions that normally prevent renderer-initiated navigations to these sensitive schemes.

The core issue is a missing security check in the browser process for the client_side_redirect_url field within blink::mojom::BeginNavigationParams. While other navigation parameters (like common_params.url and begin_params.searchable_form_url) are properly validated and filtered via RenderProcessHost::FilterURL or ChildProcessSecurityPolicy checks, client_side_redirect_url is passed through completely unfiltered.

Once a navigation completes with this poisoned parameter, the browser stores the malicious URL as the original_request_url in the active NavigationEntry. If the user performs a common UI action that reloads the original URL—such as toggling “Request Desktop Site” on mobile or switching device postures on ChromeOS—the browser executes a new navigation to the poisoned URL. Because this new navigation is initiated by browser UI code (NavigationControllerImpl::LoadOriginalRequestURL), it defaults to is_renderer_initiated = false, treating the navigation as highly trusted and bypassing all renderer sandbox restrictions.

Potential Steps to Reproduce

(Note: These are suggested steps based on static code analysis; our tooling cannot currently execute a live proof-of-concept.)

  1. Initiate Malicious Navigation: A compromised renderer sends a BeginNavigation IPC with:
    • common_params.url = 'https://attacker.com/landing'
    • begin_params.client_side_redirect_url = 'chrome://settings/' (or another privileged scheme).
  2. Bypass Filtering: In the browser process, RenderFrameHostImpl::BeginNavigation receives the IPC. It fails to filter or validate begin_params->client_side_redirect_url.
  3. Poison Redirect Chain: During NavigationRequest::StartNavigation(), the browser checks if client_side_redirect_url is not empty and pushes it as the first element (redirect_chain_[0]) of the navigation’s redirect chain.
  4. Commit Poisoned State: When the navigation to https://attacker.com/landing commits, NavigationControllerImpl saves the original request URL to the NavigationEntry. It calls NavigationRequest::GetOriginalRequestURL(), which returns redirect_chain_[0] (chrome://settings/).
  5. Wait for User Interaction: The attacker’s page is now loaded. The exploit waits for a standard browser UI action that triggers a reload of the original URL (e.g., the user tapping “Request Desktop Site” on Android, or physically exiting tablet mode on a ChromeOS device).
  6. Trigger Privilege Escalation: The UI action triggers NavigationControllerImpl::LoadOriginalRequestURL(). This method reads the poisoned original_request_url (chrome://settings/) from the NavigationEntry and constructs a LoadURLParams object.
  7. Bypass CPSP: LoadURLParams explicitly defaults is_renderer_initiated to false. NavigationControllerImpl calls LoadURLWithParams, initiating a fully browser-trusted navigation to chrome://settings/. ChildProcessSecurityPolicy checks are bypassed, and the privileged WebUI commits successfully, breaking the web sandbox.

Suggested Fix

  1. Add Filtering/Validation: In RenderFrameHostImpl::BeginNavigation, explicitly validate and filter begin_params->client_side_redirect_url using GetProcess()->FilterURL(true, &begin_params->client_side_redirect_url) or via explicit ChildProcessSecurityPolicy::CanRequestURL checks, ensuring a compromised renderer cannot claim a client-side redirect from a scheme it is not allowed to access.
  2. Defense in Depth: Consider dropping client_side_redirect_url entirely if it specifies a privileged scheme that the originating renderer process does not have access to.

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