CVE-2026-7967
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ClientSideRedirectUrlReplacercontent/browser/security_exploit_browsertest.cc |
modified | |
ifcontent/browser/security_exploit_browsertest.cc |
modified | |
RemoteFrameHostInterceptorcontent/browser/security_exploit_browsertest.cc |
modified |
Files Changed
content/browser/renderer_host/render_frame_host_impl.cccontent/browser/security_exploit_browsertest.cc
Patch
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:
Regression Test / PoC
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:
Original Bug Report
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.cccontent/browser/renderer_host/render_frame_host_impl.cccontent/browser/renderer_host/ipc_utils.cccontent/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.)
- Initiate Malicious Navigation: A compromised renderer sends a
BeginNavigationIPC with:common_params.url = 'https://attacker.com/landing'begin_params.client_side_redirect_url = 'chrome://settings/'(or another privileged scheme).
- Bypass Filtering: In the browser process,
RenderFrameHostImpl::BeginNavigationreceives the IPC. It fails to filter or validatebegin_params->client_side_redirect_url. - Poison Redirect Chain: During
NavigationRequest::StartNavigation(), the browser checks ifclient_side_redirect_urlis not empty and pushes it as the first element (redirect_chain_[0]) of the navigation’s redirect chain. - Commit Poisoned State: When the navigation to
https://attacker.com/landingcommits,NavigationControllerImplsaves the original request URL to theNavigationEntry. It callsNavigationRequest::GetOriginalRequestURL(), which returnsredirect_chain_[0](chrome://settings/). - 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).
- Trigger Privilege Escalation: The UI action triggers
NavigationControllerImpl::LoadOriginalRequestURL(). This method reads the poisonedoriginal_request_url(chrome://settings/) from theNavigationEntryand constructs aLoadURLParamsobject. - Bypass CPSP:
LoadURLParamsexplicitly defaultsis_renderer_initiatedtofalse.NavigationControllerImplcallsLoadURLWithParams, initiating a fully browser-trusted navigation tochrome://settings/.ChildProcessSecurityPolicychecks are bypassed, and the privileged WebUI commits successfully, breaking the web sandbox.
Suggested Fix
- Add Filtering/Validation: In
RenderFrameHostImpl::BeginNavigation, explicitly validate and filterbegin_params->client_side_redirect_urlusingGetProcess()->FilterURL(true, &begin_params->client_side_redirect_url)or via explicitChildProcessSecurityPolicy::CanRequestURLchecks, ensuring a compromised renderer cannot claim a client-side redirect from a scheme it is not allowed to access. - Defense in Depth: Consider dropping
client_side_redirect_urlentirely 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.