CVE-2026-87465
Overview
Background
- `DownloadURLParams` (`frame.mojom`)
- the Mojo struct a renderer sends to the browser over the
DownloadURLIPC to request a download; its fields are renderer-supplied and therefore untrusted. - `is_context_menu_save` → `should_prompt_for_save_location`
- the boolean flag being renamed; previously named as if it told the browser the download came from a context-menu “Save as”, now named for its sole remaining use: whether to show the file picker.
- `content_initiated`
- a browser-side
DownloadUrlParametersproperty indicating the download originated from web content, used in download authorization/handling decisions. - `RenderFrameHostImpl::DownloadURL`
- the browser-process IPC handler that turns a renderer’s
DownloadURLParamsinto adownload::DownloadUrlParametersand starts the download.
Root Cause Analysis
is_context_menu_save field was named and historically used as if it authoritatively indicated the download’s source (a context-menu save), but a compromised renderer can set any value it likes for it. Per the commit message, a prior change (crrev.com/c/8177729) already narrowed the field so that in the browser’s DownloadURL handler it is used only to drive parameters->set_prompt(...) (whether the save-location file picker is shown), while parameters->set_content_initiated(true) is set unconditionally for downloads arriving through this IPC — so the browser no longer trusts the field to decide the download source. This CL is the hardening follow-up: it renames the field to should_prompt_for_save_location across the Mojo definition and all callers, updates the handler to call set_prompt(blink_parameters->should_prompt_for_save_location), and rewrites the comment to drop the reference to treating the value as a source indicator. The rename works as a fix by removing the misleading name that invited the browser to (again) rely on a renderer-controlled flag as a trustworthy source signal, keeping the flag’s authority limited to the benign UI choice of prompting.should_prompt_for_save_location so the browser is not tempted to make authorization decisions from a value the renderer can forge.Attack Path
- Compromised renderer crafts the IPC
An attacker with a compromised renderer builds a
DownloadURLParamsfor theDownloadURLIPC and sets the source flag to any value, since the field is renderer-supplied. - Historic reliance on the flag
Before the narrowing in
crrev.com/c/8177729, the browser could readis_context_menu_saveas an indication of the download’s source, letting a lying renderer misrepresent a web-content-initiated download as a user-driven context-menu save (inferredas the pre-narrowing behavior the commit describes). - Source misclassification affects handling
Such misclassification could influence download authorization/handling that keys on whether a download is content-initiated versus user-initiated (
inferred). - Hardening removes the foothold
This CL forces
set_content_initiated(true)unconditionally for this IPC and renames the field toshould_prompt_for_save_location, so the only effect a renderer retains is toggling whether the save-location picker appears.
Impact Assessment
content_initiated unconditionally, so the renderer’s only retained influence is whether the file-picker prompt is shown; the change is a defense-in-depth rename to prevent future misuse. The advisory classifies it as incorrect authorization in Downloads and does not report in-the-wild exploitation; the concrete pre-narrowing exploitation impact is (inferred) from the commit description rather than demonstrated by this diff.Files Changed
third_party/blink/renderer/core/html/media/html_video_element_test.cccontent/browser/download/download_browsertest.cccontent/browser/renderer_host/render_frame_host_impl.ccthird_party/blink/public/mojom/frame/frame.mojomthird_party/blink/renderer/core/frame/local_frame.ccthird_party/blink/renderer/core/html/media/html_video_element.cc
Audit Directions
- Renderer-supplied flags used as trust/authorization signalsSearch Mojo structs sent from the renderer for booleans/enums named after a provenance (“from context menu”, “user initiated”) and verify the browser never uses them to make security decisions, only benign UI choices.
- Names that imply trust the value cannot carryLook for fields whose names suggest a trusted source but whose data crosses a privilege boundary; misleading names invite future code to re-introduce reliance on them.
- Download source vs. UI-prompt conflationAudit download-initiation paths (
DownloadURL, context-menu saves, media/data-URL saves) to confirmcontent_initiatedand similar provenance is determined by the browser, not derived from the renderer-supplied parameters.
Patch
From 0d84f06e082c9d50591ae52473a1f8afbd6985df Mon Sep 17 00:00:00 2001 From: Svend L <[email protected]> Date: Fri, 14 Aug 2026 11:15:45 -0700 Subject: [PATCH] [Downloads] Rename is_context_menu_save to should_prompt_for_save_location After crrev.com/c/8177729, DownloadURLParams::is_context_menu_save is only used to determine whether the filepicker should be shown for a download. Since the param is supplied by the renderer, a compromised renderer can lie about whether a download is initiated from a context menu. Rename the param to reflect its narrower scope, and to discourage the browser from relying on the param value to indicate download source in the future. Bug: 496616790 Change-Id: Ic5a1277c2ded0671b3f72f9ae004d81a6a6a6964 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8204664 Reviewed-by: Charlie Reis <[email protected]> Reviewed-by: Giovanni Ortuno Urquidi <[email protected]> Reviewed-by: Nate Chapin <[email protected]> Reviewed-by: Ari Chivukula <[email protected]> Reviewed-by: Min Qin <[email protected]> Commit-Queue: Svend L <[email protected]> Cr-Commit-Position: refs/heads/main@{#1679710} --- diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc index e038883..c8440e1 100644 --- a/content/browser/renderer_host/render_frame_host_impl.cc +++ b/content/browser/renderer_host/render_frame_host_impl.cc @@ -8003,14 +8003,13 @@ std::unique_ptr<download::DownloadUrlParameters> parameters = CreateDownloadUrlParameters(blink_parameters->url, traffic_annotation); // Downloads arriving through this IPC handler always originate from web - // content, so treat them as content-initiated regardless of what the - // renderer reports in `is_context_menu_save`. + // content. parameters->set_content_initiated(true); parameters->set_has_user_gesture(blink_parameters->has_user_gesture && HasTransientUserActivation()); parameters->set_suggested_name( blink_parameters->suggested_name.value_or(std::u16string())); - parameters->set_prompt(blink_parameters->is_context_menu_save); + parameters->set_prompt(blink_parameters->should_prompt_for_save_location); parameters->set_cross_origin_redirects( blink_parameters->cross_origin_redirects); parameters->set_referrer( diff --git a/third_party/blink/public/mojom/frame/frame.mojom b/third_party/blink/public/mojom/frame/frame.mojom index 5db65454..579f2fd 100644 --- a/third_party/blink/public/mojom/frame/frame.mojom +++ b/third_party/blink/public/mojom/frame/frame.mojom @@ -142,8 +142,8 @@ // Non-null when |url| is for "data:", eg. when saving an image. pending_remote<Blob>? data_url_blob; - // Whether the download is from context menu. - bool is_context_menu_save = false; + // Whether to prompt the user for a destination filepath. + bool should_prompt_for_save_location = false; // True when the download was preceded by a recent gesture from the user. bool has_user_gesture = false; diff --git a/third_party/blink/renderer/core/frame/local_frame.cc b/third_party/blink/renderer/core/frame/local_frame.cc index 43eb225..f049ab6f 100644 --- a/third_party/blink/renderer/core/frame/local_frame.cc +++ b/third_party/blink/renderer/core/frame/local_frame.cc @@ -3867,7 +3867,7 @@ } auto params = mojom::blink::DownloadURLParams::New(); - params->is_context_menu_save = true; + params->should_prompt_for_save_location = true; params->data_url_blob = DataURLToBlob(url); GetLocalFrameHostRemote().DownloadURL(std::move(params)); } diff --git a/third_party/blink/renderer/core/html/media/html_video_element.cc b/third_party/blink/renderer/core/html/media/html_video_element.cc index d47b1d93..e07540f 100644 --- a/third_party/blink/renderer/core/html/media/html_video_element.cc +++ b/third_party/blink/renderer/core/html/media/html_video_element.cc @@ -574,7 +574,7 @@ /*is_top_level_navigation=*/false); auto params = mojom::blink::DownloadURLParams::New(); - params->is_context_menu_save = true; + params->should_prompt_for_save_location = true; auto timestamp_ms = base::saturated_cast<uint32_t>( currentTime() * base::Time::kMillisecondsPerSecond); params->suggested_name = diff --git a/content/browser/download/download_browsertest.cc b/content/browser/download/download_browsertest.cc index 5f0208a..9e57315f 100644 --- a/content/browser/download/download_browsertest.cc +++ b/content/browser/download/download_browsertest.cc @@ -5446,7 +5446,7 @@ auto params = blink::mojom::DownloadURLParams::New(); params->url = kDownloadUrl; - params->is_context_menu_save = true; + params->should_prompt_for_save_location = true; static_cast<RenderFrameHostImpl*>(render_frame_host) ->DownloadURL(std::move(params)); @@ -5520,7 +5520,7 @@ auto params = blink::mojom::DownloadURLParams::New(); params->url = kDownloadUrl; - params->is_context_menu_save = true; + params->should_prompt_for_save_location = true; static_cast<RenderFrameHostImpl*>(fenced_frame_host) ->DownloadURL(std::move(params)); diff --git a/third_party/blink/renderer/core/html/media/html_video_element_test.cc b/third_party/blink/renderer/core/html/media/html_video_element_test.cc index 18d3482b..e7bfeee 100644 --- a/third_party/blink/renderer/core/html/media/html_video_element_test.cc +++ b/third_party/blink/renderer/core/html/media/html_video_element_test.cc @@ -710,7 +710,7 @@ ASSERT_TRUE(FrameHost()->download_url_called()); const auto& params = FrameHost()->download_params(); ASSERT_TRUE(params); - EXPECT_TRUE(params->is_context_menu_save); + EXPECT_TRUE(params->should_prompt_for_save_location); EXPECT_TRUE(params->suggested_name.starts_with("videoframe_")); EXPECT_TRUE(params->url.ProtocolIs("blob")); EXPECT_TRUE(params->blob_url_token.is_valid());
Regression Test / PoC
diff --git a/content/browser/download/download_browsertest.cc b/content/browser/download/download_browsertest.cc
index 5f0208a..9e57315f 100644
--- a/content/browser/download/download_browsertest.cc
+++ b/content/browser/download/download_browsertest.cc
@@ -5446,7 +5446,7 @@
auto params = blink::mojom::DownloadURLParams::New();
params->url = kDownloadUrl;
- params->is_context_menu_save = true;
+ params->should_prompt_for_save_location = true;
static_cast<RenderFrameHostImpl*>(render_frame_host)
->DownloadURL(std::move(params));
@@ -5520,7 +5520,7 @@
auto params = blink::mojom::DownloadURLParams::New();
params->url = kDownloadUrl;
- params->is_context_menu_save = true;
+ params->should_prompt_for_save_location = true;
static_cast<RenderFrameHostImpl*>(fenced_frame_host)
->DownloadURL(std::move(params));
diff --git a/third_party/blink/renderer/core/html/media/html_video_element_test.cc b/third_party/blink/renderer/core/html/media/html_video_element_test.cc
index 18d3482b..e7bfeee 100644
--- a/third_party/blink/renderer/core/html/media/html_video_element_test.cc
+++ b/third_party/blink/renderer/core/html/media/html_video_element_test.cc
@@ -710,7 +710,7 @@
ASSERT_TRUE(FrameHost()->download_url_called());
const auto& params = FrameHost()->download_params();
ASSERT_TRUE(params);
- EXPECT_TRUE(params->is_context_menu_save);
+ EXPECT_TRUE(params->should_prompt_for_save_location);
EXPECT_TRUE(params->suggested_name.starts_with("videoframe_"));
EXPECT_TRUE(params->url.ProtocolIs("blob"));
EXPECT_TRUE(params->blob_url_token.is_valid());
Original Bug Report
BFCache and download throttle bypass via renderer-controlled is_context_menu_save IPC
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A compromised renderer can bypass BackForwardCache (BFCache) activity checks and automatic download throttles by spoofing the is_context_menu_save flag in the DownloadURL IPC. This allows an inactive frame to trigger a Save As dialog attributed to the currently visible tab. It also enables spoofing of Mark-of-the-Web (MOTW) referrer metadata.
Affected files:
content/browser/download/download_manager_impl.cccontent/browser/renderer_host/render_frame_host_impl.cccontent/browser/renderer_host/ipc_utils.ccthird_party/blink/public/mojom/frame/frame.mojom
Estimated timestamp from git blame: 2024-12-10
Description
The DownloadURL IPC call in RenderFrameHostImpl handles download requests from the renderer. One of the parameters in blink::mojom::DownloadURLParams is is_context_menu_save, which is intended to indicate if the download was initiated via a user right-clicking and selecting ‘Save Link As…’.
In RenderFrameHostImpl::DownloadURL, the content_initiated flag is calculated as !blink_parameters->is_context_menu_save. This flag is then passed to DownloadManagerImpl::BeginDownloadInternal.
In DownloadManagerImpl::BeginDownloadInternal, several critical security checks are gated behind the content_initiated flag:
if (rfh && content_initiated) {
if (rfh->IsInactiveAndDisallowActivation(
RenderFrameHost::InactiveCheckAllowedReason::kBeginDownload)) {
return;
}
// ... throttle checks ...
if (!delegate_->CheckDownloadAllowed(...)) {
return;
}
}
A compromised renderer can arbitrarily set is_context_menu_save to true, forcing content_initiated to be false. This results in the following security bypasses:
- BFCache Activity Gate Bypass: The call to
IsInactiveAndDisallowActivationis skipped. This allows aRenderFrameHostthat is currently in the BackForwardCache (and thus should be inactive) to successfully initiate a download. - Automatic Download Throttle Bypass: The
delegate_->CheckDownloadAllowedcall, which enforces automatic download throttles and user gesture requirements, is skipped. - UI Spoofing: Because the download is initiated from a BFCached frame while a new page is visible in the tab, the ‘Save As’ dialog (triggered because
promptis set totruewhenis_context_menu_saveis true) appears while the user is looking at a different origin. Thetab_urlfor the download is taken from the current visible entry, leading to incorrect attribution in the download UI. - Mark-of-the-Web (MOTW) Spoofing: The renderer-provided
referrerURL is not validated against the frame’s origin inVerifyDownloadUrlParams. This unvalidated URL is eventually used to set the Mark-of-the-Web (MOTW)ReferrerUrlandHostUrlon Windows (e.g., viaBaseFile::GetEffectiveAuthorityURLfordata:URLs and thenquarantine::SetInternetZoneIdentifierDirectly), allowing an attacker to falsely attribute a malicious payload to a trusted site.
Potential Reproduction Steps (AI Suggested)
Note: These are suggested steps, as this AI agent does not have the ability to run code to verify them.
- Obtain renderer RCE in a process hosting
attacker.com. - Navigate a tab from
attacker.comto a trusted site likehttps://victim.com/. Theattacker.comRenderFrameHostenters the BFCache. - From the compromised
attacker.comrenderer process, craft ablink::mojom::DownloadURLParamsstruct. - Set
is_context_menu_save = true,urlto a malicious payload (e.g., adata:URI containing an executable), andreferrertohttps://victim.com/. - Send this struct to the browser process via the
blink::mojom::LocalFrameHost::DownloadURLIPC endpoint. - The browser will skip the BFCache check and prompt the user with a ‘Save As’ dialog while
victim.comis in the address bar. - Upon completion, the downloaded file’s Mark-of-the-Web
Zone.Identifieron Windows will containReferrerUrl=https://victim.com/andHostUrl=https://victim.com/.
Suggested Fix
The browser process should not trust the is_context_menu_save flag from the renderer for critical security checks like BFCache gating and download throttling. Context menu saves are typically initiated by the browser process itself. If this IPC must be used for context menu saves, the browser process should independently verify that a valid context menu action recently occurred for that frame. Additionally, the referrer provided in DownloadURLParams should be validated against the requesting frame’s origin in VerifyDownloadUrlParams to prevent MOTW spoofing.
Evaluated with Chrome root at commit: bb48272cafb7e24c93f55ef40da398cd206ee651
Results 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. Please feel free to reach out to me if you have concerns or feedback.