Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Downloads
DescriptionInformation leak in Downloads
ComponentDownloads
Bug ClassLogic Error
Tracker517178299
Fix commit3558bf2bb5f1 (chromium/src) +127/-9
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

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

Files Changed

  • content/browser/renderer_host/navigation_request.cc
  • content/browser/renderer_host/navigation_request.h
  • content/browser/renderer_host/navigation_request_browsertest.cc
From 3558bf2bb5f17de8f6a80cad45d7412c7f61e6ea Mon Sep 17 00:00:00 2001
From: Liam Brady <[email protected]>
Date: Tue, 04 Aug 2026 13:34:24 -0700
Subject: [PATCH] Recompute OpenerCrossOrigin download policy in browser.

This CL recomputes the OpenerCrossOrigin download policy inside
NavigationRequest on the browser side. This prevents a compromised
renderer from bypassing cross-origin opener download restrictions by
tampering with IPC parameters.

This CL also sanitizes the URL logged to the console for cross-origin
opener download deprecation to only include the serialized origin,
avoiding leakage of sensitive paths/tokens in query parameters.

Bug: 517178299
Change-Id: I829b4e653c3e0c7f9c6b6c5f35987bb51ce49d31
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8172183
Commit-Queue: Liam Brady <[email protected]>
Reviewed-by: Alex Moshchuk <[email protected]>
Reviewed-by: Min Qin <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1673634}
---

diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
index f48b3b1..bd3801e1 100644
--- a/content/browser/renderer_host/navigation_request.cc
+++ b/content/browser/renderer_host/navigation_request.cc
@@ -1836,6 +1836,15 @@
   }
 #endif
 
+  if (GetInitiatorFrameToken().has_value()) {
+    RenderFrameHostImpl* initiator_rfh = RenderFrameHostImpl::FromFrameToken(
+        GetInitiatorProcessId(), GetInitiatorFrameToken().value());
+    if (initiator_rfh) {
+      initiator_document_token_ = initiator_rfh->GetDocumentToken();
+      is_opener_navigation_ =
+          (initiator_rfh->frame_tree_node()->opener() == frame_tree_node_);
+    }
+  }
 
   ComputeDownloadPolicy();
 
@@ -1847,13 +1856,6 @@
       perfetto::protos::pbzero::ChromeTrackEvent::kNavigation, this);
   TRACE_EVENT_BEGIN("navigation", "Initializing", GetNavigationTracingTrack());
 
-  if (GetInitiatorFrameToken().has_value()) {
-    RenderFrameHostImpl* initiator_rfh = RenderFrameHostImpl::FromFrameToken(
-        GetInitiatorProcessId(), GetInitiatorFrameToken().value());
-    if (initiator_rfh)
-      initiator_document_token_ = initiator_rfh->GetDocumentToken();
-  }
-
   // Spec: https://github.com/whatwg/html/issues/8846
   // We only allow the parent to access a subframe resource timing if the
   // navigation is container-initiated, e.g. iframe changed src.
@@ -8649,7 +8651,7 @@
             "Navigating a cross-origin opener to a download (%s) is "
             "deprecated, see "
             "https://www.chromestatus.com/feature/5742188281462784.",
-            common_params_->url.spec().c_str()));
+            common_params_->url.DeprecatedGetOriginAsURL().spec().c_str()));
     GetContentClient()->browser()->LogWebFeatureForCurrentPage(
         rfh, blink::mojom::WebFeature::kOpenerNavigationDownloadCrossOrigin);
   }
@@ -12247,12 +12249,21 @@
     download_policy().SetDisallowed(blink::NavigationDownloadType::kSandbox);
   }
 
+  // [OpenerCrossOrigin]
+  bool is_cross_origin =
+      GetInitiatorOrigin() && !GetInitiatorOrigin()->IsSameOriginWith(
+                                  frame_tree_node_->current_origin());
+
+  if (is_opener_navigation_ && is_cross_origin) {
+    download_policy().SetDisallowed(
+        blink::NavigationDownloadType::kOpenerCrossOrigin);
+  }
+
   // TODO(arthursonzogni): Check if the following fields from the
   // NavigationDownloadPolicy could be computed here from the browser process
   // instead:
   //
   // [NoGesture]
-  // [OpenerCrossOrigin]
   // [AdFrameNoGesture]
   // [AdFrame]
   // [Interstitial]
diff --git a/content/browser/renderer_host/navigation_request.h b/content/browser/renderer_host/navigation_request.h
index 2ed8127a..de88c2f 100644
--- a/content/browser/renderer_host/navigation_request.h
+++ b/content/browser/renderer_host/navigation_request.h
@@ -1904,6 +1904,9 @@
 
  private:
   friend class NavigationRequestTest;
+  FRIEND_TEST_ALL_PREFIXES(
+      NavigationRequestDownloadBrowserTest,
+      OpenerCrossOrigin_BrowserOverridesCompromisedRenderer);
   FRIEND_TEST_ALL_PREFIXES(NavigationRequestTest, SanitizeRedirectsForCommit);
   FRIEND_TEST_ALL_PREFIXES(NavigationRequestTest,
                            SanitizeRedirectsForCommitRelativeLocation);
@@ -3220,6 +3223,10 @@
   // initiated navigations which use `CreateBrowserInitiated()`.
   const bool was_opener_suppressed_ = false;
 
+  // Indicates whether the initiator is navigating its opener frame at the time
+  // of request creation.
+  bool is_opener_navigation_ = false;
+
   // This tracks a connection between the current pending entry and this
   // request, such that the pending entry can be discarded if no requests are
   // left referencing it.
diff --git a/content/browser/renderer_host/navigation_request_browsertest.cc b/content/browser/renderer_host/navigation_request_browsertest.cc
index 197641d..ca27378 100644
--- a/content/browser/renderer_host/navigation_request_browsertest.cc
+++ b/content/browser/renderer_host/navigation_request_browsertest.cc
@@ -15,6 +15,7 @@
 #include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
 #include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/values.h"
@@ -3826,6 +3827,105 @@
   EXPECT_FALSE(handle_observer.is_download());
 }
 
+IN_PROC_BROWSER_TEST_F(NavigationRequestDownloadBrowserTest,
+                       OpenerCrossOriginDownload_SanitizesConsoleUrl) {
+  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  GURL popup_url(embedded_test_server()->GetURL("c.com", "/title1.html"));
+  GURL download_url(embedded_test_server()->GetURL(
+      "b.com", "/download-test1.lib?token=SECRET_12345"));
+
+  ASSERT_TRUE(NavigateToURL(shell(), main_url));
+
+  WebContentsConsoleObserver console_observer(shell()->web_contents());
+  console_observer.SetPattern(
+      "*Navigating a cross-origin opener to a download*");
+
+  // Open a cross-origin popup from a.com.
+  ShellAddedObserver shell_observer;
+  std::string open_script = JsReplace("window.open($1, 'popup');", popup_url);
+  EXPECT_TRUE(ExecJs(shell(), open_script));
+  Shell* popup = shell_observer.GetShell();
+  EXPECT_TRUE(WaitForLoadStop(popup->web_contents()));
+
+  // Navigate opener (a.com) to cross-origin download from the cross-origin
+  // popup (c.com).
+  std::string script = JsReplace("window.opener.location = $1;", download_url);
+  EXPECT_TRUE(ExecJs(popup->web_contents(), script));
+
+  ASSERT_TRUE(console_observer.Wait());
+  ASSERT_EQ(1u, console_observer.messages().size());
+  std::string msg = base::UTF16ToUTF8(console_observer.messages()[0].message);
+
+  // Verify the console message contains the origin (b.com) but not full path or
+  // secret query token.
+  EXPECT_THAT(msg, ::testing::HasSubstr("b.com"));
+  EXPECT_THAT(msg, ::testing::Not(::testing::HasSubstr("SECRET_12345")));
+  EXPECT_THAT(msg, ::testing::Not(::testing::HasSubstr("download-test1.lib")));
+}
+
+IN_PROC_BROWSER_TEST_F(NavigationRequestDownloadBrowserTest,
+                       OpenerCrossOrigin_BrowserOverridesCompromisedRenderer) {
+  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  GURL popup_url(embedded_test_server()->GetURL("c.com", "/title1.html"));
+  GURL download_url(
+      embedded_test_server()->GetURL("b.com", "/download-test1.lib"));
+
+  ASSERT_TRUE(NavigateToURL(shell(), main_url));
+
+  ShellAddedObserver shell_observer;
+  std::string open_script = JsReplace("window.open($1, 'popup');", popup_url);
+  EXPECT_TRUE(ExecJs(shell(), open_script));
+  Shell* popup = shell_observer.GetShell();
+  EXPECT_TRUE(WaitForLoadStop(popup->web_contents()));
+
+  RenderFrameHostImpl* main_rfh =
+      static_cast<WebContentsImpl*>(shell()->web_contents())
+          ->GetPrimaryMainFrame();
+  RenderFrameHostImpl* popup_rfh =
+      static_cast<WebContentsImpl*>(popup->web_contents())
+          ->GetPrimaryMainFrame();
+
+  EXPECT_NE(nullptr, popup_rfh->frame_tree_node()->opener());
+  EXPECT_EQ(main_rfh->frame_tree_node(),
+            popup_rfh->frame_tree_node()->opener());
+
+  TestNavigationManager manager(shell()->web_contents(), download_url);
+  std::string script = JsReplace("window.opener.location = $1;", download_url);
+  EXPECT_TRUE(ExecJs(popup->web_contents(), script));
+
+  EXPECT_TRUE(manager.WaitForRequestStart());
+  NavigationRequest* request =
+      static_cast<WebContentsImpl*>(shell()->web_contents())
+          ->GetPrimaryMainFrame()
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/renderer_host/navigation_request_browsertest.cc b/content/browser/renderer_host/navigation_request_browsertest.cc
index 197641d..ca27378 100644
--- a/content/browser/renderer_host/navigation_request_browsertest.cc
+++ b/content/browser/renderer_host/navigation_request_browsertest.cc
@@ -15,6 +15,7 @@
 #include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
 #include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/values.h"
@@ -3826,6 +3827,105 @@
   EXPECT_FALSE(handle_observer.is_download());
 }
 
+IN_PROC_BROWSER_TEST_F(NavigationRequestDownloadBrowserTest,
+                       OpenerCrossOriginDownload_SanitizesConsoleUrl) {
+  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  GURL popup_url(embedded_test_server()->GetURL("c.com", "/title1.html"));
+  GURL download_url(embedded_test_server()->GetURL(
+      "b.com", "/download-test1.lib?token=SECRET_12345"));
+
+  ASSERT_TRUE(NavigateToURL(shell(), main_url));
+
+  WebContentsConsoleObserver console_observer(shell()->web_contents());
+  console_observer.SetPattern(
+      "*Navigating a cross-origin opener to a download*");
+
+  // Open a cross-origin popup from a.com.
+  ShellAddedObserver shell_observer;
+  std::string open_script = JsReplace("window.open($1, 'popup');", popup_url);
+  EXPECT_TRUE(ExecJs(shell(), open_script));
+  Shell* popup = shell_observer.GetShell();
+  EXPECT_TRUE(WaitForLoadStop(popup->web_contents()));
+
+  // Navigate opener (a.com) to cross-origin download from the cross-origin
+  // popup (c.com).
+  std::string script = JsReplace("window.opener.location = $1;", download_url);
+  EXPECT_TRUE(ExecJs(popup->web_contents(), script));
+
+  ASSERT_TRUE(console_observer.Wait());
+  ASSERT_EQ(1u, console_observer.messages().size());
+  std::string msg = base::UTF16ToUTF8(console_observer.messages()[0].message);
+
+  // Verify the console message contains the origin (b.com) but not full path or
+  // secret query token.
+  EXPECT_THAT(msg, ::testing::HasSubstr("b.com"));
+  EXPECT_THAT(msg, ::testing::Not(::testing::HasSubstr("SECRET_12345")));
+  EXPECT_THAT(msg, ::testing::Not(::testing::HasSubstr("download-test1.lib")));
+}
+
+IN_PROC_BROWSER_TEST_F(NavigationRequestDownloadBrowserTest,
+                       OpenerCrossOrigin_BrowserOverridesCompromisedRenderer) {
+  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  GURL popup_url(embedded_test_server()->GetURL("c.com", "/title1.html"));
+  GURL download_url(
+      embedded_test_server()->GetURL("b.com", "/download-test1.lib"));
+
+  ASSERT_TRUE(NavigateToURL(shell(), main_url));
+
+  ShellAddedObserver shell_observer;
+  std::string open_script = JsReplace("window.open($1, 'popup');", popup_url);
+  EXPECT_TRUE(ExecJs(shell(), open_script));
+  Shell* popup = shell_observer.GetShell();
+  EXPECT_TRUE(WaitForLoadStop(popup->web_contents()));
+
+  RenderFrameHostImpl* main_rfh =
+      static_cast<WebContentsImpl*>(shell()->web_contents())
+          ->GetPrimaryMainFrame();
+  RenderFrameHostImpl* popup_rfh =
+      static_cast<WebContentsImpl*>(popup->web_contents())
+          ->GetPrimaryMainFrame();
+
+  EXPECT_NE(nullptr, popup_rfh->frame_tree_node()->opener());
+  EXPECT_EQ(main_rfh->frame_tree_node(),
+            popup_rfh->frame_tree_node()->opener());
+
+  TestNavigationManager manager(shell()->web_contents(), download_url);
+  std::string script = JsReplace("window.opener.location = $1;", download_url);
+  EXPECT_TRUE(ExecJs(popup->web_contents(), script));
+
+  EXPECT_TRUE(manager.WaitForRequestStart());
+  NavigationRequest* request =
+      static_cast<WebContentsImpl*>(shell()->web_contents())
+          ->GetPrimaryMainFrame()
+          ->frame_tree_node()
+          ->navigation_request();
+
+  // Simulate a compromised renderer that stripped the kOpenerCrossOrigin flag
+  // from IPC.
+  request->common_params_->download_policy.observed_types.reset();
+  request->common_params_->download_policy.disallowed_types.reset();
+
+  EXPECT_FALSE(request->common_params_->download_policy.IsType(
+      blink::NavigationDownloadType::kOpenerCrossOrigin));
+  EXPECT_TRUE(request->common_params_->download_policy.IsDownloadAllowed());
+
+  // Close the initiator popup to destroy the initiator RenderFrameHost. This
+  // verifies that browser-side enforcement relies on cached state recorded at
+  // NavigationRequest creation, preventing a race condition bypass where a
+  // compromised renderer closes itself or navigates away before policy
+  // recomputation.
+  popup->Close();
+
+  // Run browser recomputation:
+  request->ComputeDownloadPolicy();
+
+  // Verify browser recomputation enforced kOpenerCrossOrigin even after
+  // initiator RFH destruction:
+  EXPECT_TRUE(request->common_params_->download_policy.IsType(
+      blink::NavigationDownloadType::kOpenerCrossOrigin));
+  EXPECT_FALSE(request->common_params_->download_policy.IsDownloadAllowed());
+}
+
 class NavigationRequestBackForwardBrowserTest
     : public NavigationRequestBrowserTest,
       public WebContentsObserver {
Loading diff…

Original Bug Report

reported by [email protected]

Site-Isolation bypass: Renderer-forgeable download policy leaks post-redirect URL

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential security vulnerability exists where a compromised renderer can forge the kOpenerCrossOrigin download policy bit due to a lack of browser-side validation. When a navigation is redirected to a cross-origin download, the browser process potentially leaks the full, unsanitized post-redirect URL to the pre-navigation frame via a console message. This could allow an attacker to obtain sensitive query parameters or capability tokens from cross-origin download URLs.

Affected files:

  • content/browser/renderer_host/navigation_request.cc

Estimated timestamp from git blame: 2019-02-21

Root Cause Analysis

In content/browser/renderer_host/navigation_request.cc, the function NavigationRequest::RecordDownloadUseCountersPrePolicyCheck() is responsible for logging deprecation warnings when a download occurs under certain conditions.

Specifically, if the download policy is flagged as kOpenerCrossOrigin, the browser writes a warning to the console of the pre-navigation frame (rfh):

void NavigationRequest::RecordDownloadUseCountersPrePolicyCheck() {
  RenderFrameHost* rfh = frame_tree_node_->current_frame_host();
  ...
  if (download_policy().IsType(
          blink::NavigationDownloadType::kOpenerCrossOrigin)) {
    rfh->AddMessageToConsole(
        blink::mojom::ConsoleMessageLevel::kError,
        base::StringPrintf(
            "Navigating a cross-origin opener to a download (%s) is "
            "deprecated, see ...",
            common_params_->url.spec().c_str())); // <-- Potential URL leak

The NavigationDownloadPolicy struct is passed from the renderer via the BeginNavigation Mojo IPC inside CommonNavigationParams. Currently, the browser process fails to validate or recompute the kOpenerCrossOrigin policy bit, trusting the value provided by the renderer. This is explicitly noted in a TODO comment in NavigationRequest::ComputeDownloadPolicy():

void NavigationRequest::ComputeDownloadPolicy() {
  ...
  // TODO(arthursonzogni): Check if the following fields from the
  // NavigationDownloadPolicy could be computed here from the browser process
  // instead:
  //
  // [NoGesture]
  // [OpenerCrossOrigin]
  // [AdFrameNoGesture]
  // [AdFrame]
  // [Interstitial]
}

If a compromised renderer maliciously triggers a navigation with kOpenerCrossOrigin set to true, the browser will evaluate the condition as true upon receiving a download response. It will then serialize and send the full, unsanitized post-redirect URL (stored in common_params_->url) back to the pre-navigation frame via LocalFrame::AddMessageToConsole Mojo IPC.

Potential Attack Steps

Please note: The analyzing tools utilized to evaluate this codebase do not have the capability to execute code. The following sequence describes potential steps an attacker might follow to trigger this vulnerability:

  1. Renderer Compromise: The attacker exploits a vulnerability in the sandboxed renderer process (e.g., a V8 memory corruption bug) to gain arbitrary code execution inside the sandbox.
  2. Parameter Forgery: The compromised renderer initiates a navigation to a target cross-origin endpoint (e.g., https://victim.com/export) while forging the observed_types.opener_cross_origin = true bit inside CommonNavigationParams::download_policy over the FrameHost::BeginNavigation Mojo interface.
  3. Redirection Handling: The request to https://victim.com/export is sent with the victim’s session cookies and redirects to a temporary, capability-locked resource containing query parameters (e.g., https://cdn.victim.com/file.zip?token=SECRET_TOKEN).
  4. Download Response: The target endpoint responds with a Content-Disposition: attachment header, which classifies the response as a download.
  5. Console Leak: The browser calls RecordDownloadUseCountersPrePolicyCheck(). Due to the forged policy bit, it prints the full URL containing token=SECRET_TOKEN to the pre-navigation console via Mojo IPC, allowing the compromised renderer to intercept and read the token.

Impact

This is a potential Site-Isolation bypass. A compromised renderer can maliciously exfiltrate sensitive query parameters, signed URLs, or session-bound capability tokens from cross-origin download URLs, leading to unauthorized access to user data.

Suggested Fix

  1. Sanitize Console URLs: In RecordDownloadUseCountersPrePolicyCheck(), strip path and query components of the printed URL using .DeprecatedGetOriginAsURL().spec() or a similar sanitization helper before printing it to the console. This is consistent with other console warnings in the same file.
  2. Browser-side Recomputation: Fully implement the TODO in NavigationRequest::ComputeDownloadPolicy() by computing the kOpenerCrossOrigin state securely in the browser process rather than relying on untrusted parameters from the renderer.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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