CVE-2025-12431
Overview
Background
- `chrome.downloads` API
- the extension API implemented in
chrome/browser/extensions/api/downloads/downloads_api.ccthat lets an extension with thedownloadspermission start, pause, resume, cancel and query downloads. - `DownloadItem` state
- a download is
IN_PROGRESS(possibly paused viaIsPaused()),INTERRUPTED,CANCELLEDorCOMPLETE, as reported byGetState(). - `DownloadItem::CanResume()`
- the download system’s own judgment of whether an item may be resumed; an interrupted download can be non-resumable depending on why it was interrupted.
- `Fault(...)` / `kNotResumable`
- the API helper pattern that, when its condition is true, sets an error string and makes the function return an error instead of performing the action.
Root Cause Analysis
The patched code is the extension-facing resume handler (the enclosing function name is not shown in the hunk; the use of params->download_id and the download_extension_errors::kNotResumable error indicate the downloads.resume implementation). Before the patch, the only resumability guard was Fault(download_item->IsPaused() && !download_item->CanResume(), ...), which consults CanResume() only for paused downloads; for a download that is INTERRUPTED or CANCELLED, IsPaused() is false, so the guard never fired and the handler went on to resume the item — as the commit message puts it, the check effectively always passed and skipped CanResume(). This violated the invariant that the extension API only resumes downloads that the download system itself considers resumable.
The fix tightens the check: it now rejects any CANCELLED download and any INTERRUPTED download for which CanResume() is false, while still allowing normal resume of paused in-progress downloads. Because the guard returns RespondNow(Error(...)) before the resume action, a non-resumable item can no longer be pushed back into progress via the extension API.
IsPaused()), so CanResume() was never consulted for the interrupted and cancelled downloads it exists to protect. The fix bases the check on GetState(), refusing cancelled downloads and interrupted downloads that CanResume() rejects.Attack Path
- Extension with downloads access
An installed extension holding the
downloadspermission identifies a download that is cancelled or interrupted in a non-resumable state. - Call `downloads.resume`
The extension invokes the resume API with that
download_id. - Guard bypassed
Pre-patch, because the item is not paused,
IsPaused() && !CanResume()evaluates false, nokNotResumableerror is raised, and the handler proceeds to resume the item. - Resume of a download the system refused to resume The download machinery is driven to resume an item that was cancelled or whose interruption reason made it non-resumable; if that interruption reflected a security block (for example a download blocked by a safety check), this could bypass that protection (inferred; the diff does not show which interruption reasons are non-resumable or what the resume path does next).
Impact Assessment
downloads permission; ordinary web content cannot call this API, and no compromised renderer is needed. The vendor rates it High severity; the provided metadata does not state in-the-wild exploitation.Files Changed
chrome/browser/extensions/api/downloads/downloads_api.cc
Audit Directions
- State predicates that don't match the guarded stateIn other
chrome.downloadshandlers (pause, cancel, open, show, accept danger), check that preconditions are expressed againstGetState()/danger type rather than a proxy likeIsPaused()that is false for the states that matter. - Extension APIs bypassing backend policyLook for extension functions that invoke
DownloadItem(or similar backend) actions directly without consulting the backend’s own permission predicate (CanResume(),CanOpenDownload(), danger/validation state), since extensions can reach states the normal UI never offers. - Cancelled/terminal objects revived via APIAudit APIs that act on an ID lookup for objects in terminal states (cancelled, blocked, removed) to ensure terminal states are explicitly rejected rather than implicitly allowed.
Patch
From f9f478196e288d68bf0b64a30fa7618a27fe1fc2 Mon Sep 17 00:00:00 2001 From: Min Qin <[email protected]> Date: Tue, 02 Sep 2025 16:44:44 -0700 Subject: [PATCH] Fix an issue when determining whether a download is resumable in extension To resume a download, the download should be in an interrupted state, rather than paused state. The current code will just always return true and skip the canResume() check. Bug: 436887350 Change-Id: Ie5cd0e7684a1eeacc4d80899098e89137dc1a85f Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6907509 Reviewed-by: David Trainor <[email protected]> Commit-Queue: Min Qin <[email protected]> Cr-Commit-Position: refs/heads/main@{#1509939} --- diff --git a/chrome/browser/extensions/api/downloads/downloads_api.cc b/chrome/browser/extensions/api/downloads/downloads_api.cc index f89b9608..4dfb639 100644 --- a/chrome/browser/extensions/api/downloads/downloads_api.cc +++ b/chrome/browser/extensions/api/downloads/downloads_api.cc @@ -1288,7 +1288,9 @@ browser_context(), include_incognito_information(), params->download_id); std::string error; if (InvalidId(download_item, &error) || - Fault(download_item->IsPaused() && !download_item->CanResume(), + Fault(download_item->GetState() == DownloadItem::CANCELLED || + (download_item->GetState() == DownloadItem::INTERRUPTED && + !download_item->CanResume()), download_extension_errors::kNotResumable, &error)) { return RespondNow(Error(std::move(error))); }
Original Bug Report
Security: Extension can download file by resuming interrupted download
SUMMARY
An extension with downloads permission can download a file:// URL by calling downloads.download(), waiting for the download to be interrupted, and then calling downloads.resume() on the interrupted download. This causes the download to be initiated by the browser, bypassing DownloadRequestUtils::IsURLSafe()/CPSP::CanRequestURL() checks.
Downloading local files is meant to be blocked when the extension does not have file:// permissions. The tabs.create() workaround to download some local files was also blocked in https://crrev.com/c/4772028 (August 2023, also see issue 40063229).
The resume bypass also works with all other ways to resume downloads, including resumes made through UI.
When chained with other bugs, such as issue 433800617 or issue 435684924, or if the attacker otherwise convinces user to upload the downloaded file, the attacker can steal the downloaded file’s contents.
VULNERABILITY DETAILS
In DownloadManagerImpl::BeginDownloadInternal()…
void DownloadManagerImpl::BeginDownloadInternal(
std::unique_ptr<download::DownloadUrlParameters> params,
scoped_refptr<network::SharedURLLoaderFactory> blob_url_loader_factory,
bool is_new_download,
const std::string& serialized_embedder_download_data) {
// Check if the renderer is permitted to request the requested URL.
if (params->render_process_host_id() >= 0 &&
!DownloadRequestUtils::IsURLSafe(params->render_process_host_id(),
params->url())) {
CreateInterruptedDownload(
std::move(params),
download::DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST,
weak_factory_.GetWeakPtr());
return;
}
// ...
}
…CPSP::CanRequestURL() is called through DownloadRequestUtils::IsURLSafe() only if render_process_host_id() >= 0.
When downloads.download() is called with a file:// URL, render_process_host_id is greater than zero, and CPSP::CanRequestURL() returns false for file:// URLs, therefore the download is interrupted.
However, when downloads.resume() is called for the interrupted download, render_process_host_id is -1, which per this comment means it’s not associated with a frame. This means the IsURLSafe()/CPSP::CanRequestURL() check is bypassed, and the file is downloaded.
Additional context
In the DownloadUrlParameters() constructor that leaves render_process_host_id at -1, there is this comment that points out the security checks bypass:
// Constructs a download not associated with a frame.
//
// It is not safe to have downloads not associated with a frame and
// this should only be done in a limited set of cases where the download URL
// has been previously vetted. A download that's initiated without
// associating it with a frame don't receive the same security checks
// as a request that's associated with one. Hence, downloads that are not
// associated with a frame should only be made for URLs that are either
// trusted or URLs that have previously been successfully issued using a
// non-privileged frame.
DownloadUrlParameters(
const GURL& url,
const net::NetworkTrafficAnnotationTag& traffic_annotation);
Unfortunately, this constructor is used in the downloads.resume() code path and most other download resume code paths: DownloadsResumeFunction::Run() calls DownloadItem::Resume() which calls DownloadItemImpl::ResumeInterruptedDownload().
ResumeInterruptedDownload() has this comment explaining why the unsafe constructor is used for resumes:
// Avoid using the WebContents even if it's still around. Resumption requests
// are consistently routed through the no-renderer code paths so that the
// request will not be dropped if the WebContents (and by extension, the
// associated renderer) goes away before a response is received.
std::unique_ptr<DownloadUrlParameters> download_params(
new DownloadUrlParameters(GetURL(), traffic_annotation));
BISECT
Likely commit 00b621f5126d538df488090c19175ee892a7161b (February 2016) which starts using the unsafe constructor in resume code path.
I’ll verify bisect later today.
VERSION
Chrome version: 139.0.7258.67 Stable, 141.0.7340.0 Canary
Operating System: Windows 10
REPRODUCTION CASE
To download a file in a user directory, the PoC first makes a throwaway download to get the username from the download path. Then we make the second download exploiting the vulnerability.
The core vulnerability itself doesn’t need user interaction to download the file, nor does it need to show an extension page or otherwise open a new tab.
I’ll upload chained scenarios in comments.
Setup:
- Install attached extension:
manifest.json,background.js,page.html,page.js. - Disable
file://URL access in the extension details page (this is enabled by default for unpacked extensions).
Scenario 1: Chrome history file
- Click extension icon to trigger download. (Note: Initiating/resuming downloads doesn’t require user interaction, so this step is only for PoC.)
- Click the page, then select the downloaded file. (Note: When chained, we don’t need user to explicitly upload file.)
Observed: Extension can download file:// URLs without file permission. When chained with other vulns, or through social engineering, user can read downloaded local file.
Expected: Extension cannot download file:// URLs without file permission.
Debug scenario
To see the attack occurring more slowly, you can modify background.js and set debug to true, which delays the resume() call by two seconds. Reminder: Disable file:// access in extension for proper repro.
Credit Information
Reporter credit: Alesandro Ortiz https://AlesandroOrtiz.com
background-2.jsbackground.jsdownload-file-resume-chained-cr.mp4download-file-resume-debug.mp4download-file-resume.mp4manifest-2.jsonmanifest.jsonpage-2.htmlpage-2.jspage.htmlpage.js
- http://crrev.com/00b621f5126d538df488090c19175ee892a7161b
- https://AlesandroOrtiz.com
- https://crrev.com/c/4772028
- https://issuetracker.google.com/issues/40063229
- https://issuetracker.google.com/issues/433800617
- https://issuetracker.google.com/issues/435684924
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/extensions/api/downloads/downloads_api.cc;l=1297;drc=27f899da22267983fb22a8caf88d300a9d49cf3d
- https://source.chromium.org/chromium/chromium/src/+/main:components/download/internal/common/download_item_impl.cc;l=2543;drc=877c4eff05eee91a64ab498b2f02928fe718278b
- https://source.chromium.org/chromium/chromium/src/+/main:components/download/internal/common/download_item_impl.cc;l=2592;drc=877c4eff05eee91a64ab498b2f02928fe718278b
- https://source.chromium.org/chromium/chromium/src/+/main:components/download/public/common/download_url_parameters.h;l=305;drc=877c4eff05eee91a64ab498b2f02928fe718278b
- https://source.chromium.org/chromium/chromium/src/+/main:components/download/public/common/download_url_parameters.h;l=78;drc=877c4eff05eee91a64ab498b2f02928fe718278b
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/download/download_manager_impl.cc;l=1520;drc=877c4eff05eee91a64ab498b2f02928fe718278b
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/download/download_request_utils.cc;l=31;drc=27f899da22267983fb22a8caf88d300a9d49cf3d