Medium watch CVSS 3.1 chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Downloads
DescriptionInformation leak in Downloads
ComponentDownloads
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker517178299
Fix commit3558bf2bb5f1 (chromium/src) +127/-9
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-08-04 (public git)
Disclosed2026-09-08
Patch gap35 days — fix public in source before the advisory

Background

OpenerCrossOrigin download policy
the restriction (blink::NavigationDownloadType::kOpenerCrossOrigin) that disallows a frame from navigating a cross-origin opener directly to a download.
`NavigationDownloadPolicy` over IPC
the download policy travels in common_params_->download_policy; historically the kOpenerCrossOrigin bit was computed in the renderer and trusted by the browser.
`NavigationRequest::ComputeDownloadPolicy()`
the browser-side function that finalizes the navigation’s download policy (already sets kSandbox restrictions there).
`DeprecatedGetOriginAsURL()`
a GURL method returning only the serialized origin (scheme + host + port), dropping path and query, used to sanitize what is logged.

Root Cause Analysis

The kOpenerCrossOrigin download restriction was effectively determined from the renderer-provided download_policy in common_params_, so a compromised renderer could strip that flag from the IPC parameters and bypass the cross-origin-opener download restriction. Separately, the deprecation console message logged the full navigation URL via common_params_->url.spec(), exposing sensitive path and query data (e.g. secret tokens) of the cross-origin download target to the page’s console.

The fix recomputes the policy in the browser: at NavigationRequest creation it caches is_opener_navigation_ (whether the initiator’s frame_tree_node()->opener() is this frame) alongside initiator_document_token_, and ComputeDownloadPolicy() now independently sets kOpenerCrossOrigin as disallowed when is_opener_navigation_ and the initiator origin is not same-origin with frame_tree_node_->current_origin(). Because is_opener_navigation_ is snapshotted at request creation, enforcement holds even if the initiator RenderFrameHost is later destroyed (the browser test closes the popup before recomputation). The logging change replaces url.spec() with url.DeprecatedGetOriginAsURL().spec(), so only the origin is emitted.

Key insight
The core mistake was trusting the renderer-supplied kOpenerCrossOrigin bit (and logging the full URL) rather than deriving the cross-origin-opener condition in the browser; the fix recomputes the restriction browser-side from cached initiator/opener state and logs only the serialized origin.

Attack Path

  1. Establish opener relationship An attacker page (a.com) opens a cross-origin popup (c.com), so the popup’s frame_tree_node()->opener() is the opener frame.
  2. Navigate opener to a cross-origin download The popup sets window.opener.location to a cross-origin download URL (b.com), which pre-patch would be gated only by the renderer-computed policy.
  3. Strip the flag (compromised renderer) A compromised renderer clears the kOpenerCrossOrigin bit from common_params_->download_policy (the test resets observed_types/disallowed_types), making the policy report the download as allowed.
  4. Bypass / leak Pre-patch the browser honored the tampered policy and permitted the otherwise-restricted cross-origin-opener download; additionally the deprecation console message leaked the download URL’s full path and query token (e.g. SECRET_12345) to the page.

Impact Assessment

The bug let a compromised renderer bypass the cross-origin-opener download restriction by tampering with IPC download-policy parameters, and separately leaked sensitive URL path/query data through a console message readable in the page context; enforcement and logging both occur in the browser process during navigation. Preconditions are an opener/popup relationship with a cross-origin navigation to a download. The advisory classifies this as Information leak in Downloads and does not report in-the-wild exploitation; the policy-bypass aspect is demonstrated by the patch’s browser test, while any further consequence is not shown (inferred).

Changed Functions

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

Files Changed

  • content/browser/renderer_host/navigation_request.cc
  • content/browser/renderer_host/navigation_request_browsertest.cc
  • content/browser/renderer_host/navigation_request.h

Audit Directions

  • Security policy computed in the renderer
    Audit navigation/download/security decisions that consume flags from renderer-supplied common_params_/IPC and ensure the browser independently recomputes them from trusted browser-side state.
  • State cached vs. recomputed under lifetime races
    Where enforcement depends on an initiator or opener that can be destroyed mid-navigation, verify the deciding state is snapshotted at request creation so a renderer cannot invalidate it before recomputation.
  • URLs in logs/console messages
    Review console and log emissions that include full URLs for cross-context exposure of path/query secrets, preferring origin-only serialization (DeprecatedGetOriginAsURL) when the full URL is not required.
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()
+          ->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…

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