CVE-2026-87503
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ASSERT_TRUEcomponents/download/internal/common/download_item_impl_unittest.cc |
modified | |
TEST_Fcomponents/download/internal/common/download_item_impl_unittest.cc |
modified |
Files Changed
components/download/internal/common/download_item_impl_delegate.cccomponents/download/internal/common/download_item_impl_unittest.cc
Patch
From 79649f9623c5d46ec339b1c1bc34bc4d490d56c1 Mon Sep 17 00:00:00 2001
From: Yaw Frempong <yawfrempong@google.com>
Date: Thu, 30 Jul 2026 14:02:44 -0700
Subject: [PATCH] [Downloads] Defer completion while content check is pending
DownloadItemImplDelegate::ShouldCompleteDownload() is the gate
DownloadItemImpl consults before renaming a download to its final path.
The base implementation returned true unconditionally.
InProgressDownloadManager inherits this stub, so on Android when a
download with danger type MAYBE_DANGEROUS_CONTENT is resumed in
minimal-browser mode it could reach RenameAndAnnotate() before its
content check ran (that danger type returns false for IsDangerous()).
Make the base implementation return false for the content-check-pending
danger types so the download stays IN_PROGRESS until a delegate that can
run the check takes over. DownloadManagerImpl already overrides this
method, so full-browser behaviour is unchanged.
Reviewed in https://crrev.com/i/9585676
Bug: 497986036
Change-Id: I391289156e8aa68eeaeadbfc6cd4f886aba906d3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8164176
Reviewed-by: Min Qin <qinmin@chromium.org>
Commit-Queue: Yaw Frempong <yawfrempong@google.com>
Cr-Commit-Position: refs/heads/main@{#1671401}
---
diff --git a/components/download/internal/common/download_item_impl_delegate.cc b/components/download/internal/common/download_item_impl_delegate.cc
index 53c3082..797254c 100644
--- a/components/download/internal/common/download_item_impl_delegate.cc
+++ b/components/download/internal/common/download_item_impl_delegate.cc
@@ -48,7 +48,17 @@
bool DownloadItemImplDelegate::ShouldCompleteDownload(
DownloadItemImpl* download,
base::OnceClosure complete_callback) {
- return true;
+ // The default delegate has no way to run a content check, so defer
+ // completion of downloads whose danger type still indicates a pending
+ // verdict until a delegate that can resolve it takes over.
+ switch (download->GetDangerType()) {
+ case DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT:
+ case DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING:
+ case DOWNLOAD_DANGER_TYPE_ASYNC_LOCAL_PASSWORD_SCANNING:
+ return false;
+ default:
+ return true;
+ }
}
bool DownloadItemImplDelegate::ShouldOpenDownload(
diff --git a/components/download/internal/common/download_item_impl_unittest.cc b/components/download/internal/common/download_item_impl_unittest.cc
index e368d7e..bd52c17 100644
--- a/components/download/internal/common/download_item_impl_unittest.cc
+++ b/components/download/internal/common/download_item_impl_unittest.cc
@@ -27,6 +27,7 @@
#include "base/task/single_thread_task_runner.h"
#include "base/test/gmock_move_support.h"
#include "base/test/metrics/histogram_tester.h"
+#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/threading/thread.h"
@@ -2015,6 +2016,37 @@
EXPECT_EQ(DownloadItem::COMPLETE, item->GetState());
}
+// The default DownloadItemImplDelegate (used when no embedder-level delegate
+// is attached, e.g. by InProgressDownloadManager) must defer completion of a
+// download whose content check is still pending so the file is not renamed to
+// its final path before the check resolves.
+TEST_F(DownloadItemTest,
+ DefaultDelegateDefersCompletionForPendingContentCheck) {
+ DownloadItemImpl* item = CreateDownloadItem();
+ MockDownloadFile* download_file =
+ DoIntermediateRename(item, DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT);
+ EXPECT_FALSE(item->IsDangerous());
+ EXPECT_EQ(DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT,
+ item->GetDangerType());
+
+ EXPECT_CALL(*mock_delegate(), ShouldCompleteDownload_(item, _))
+ .WillRepeatedly([&](DownloadItemImpl* download, base::OnceClosure& cb) {
+ return mock_delegate()
+ ->DownloadItemImplDelegate::ShouldCompleteDownload(download,
+ std::move(cb));
+ });
+ EXPECT_CALL(*download_file, RenameAndAnnotate(_, _, _, _, _, _, _)).Times(0);
+ item->DestinationObserverAsWeakPtr()->DestinationCompleted(
+ 0, std::unique_ptr<crypto::SecureHash>());
+ ASSERT_TRUE(base::test::RunUntil([&]() { return item->AllDataSaved(); }));
+
+ EXPECT_TRUE(item->AllDataSaved());
+ EXPECT_EQ(DownloadItem::IN_PROGRESS, item->GetState());
+ EXPECT_EQ(DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT,
+ item->GetDangerType());
+ CleanupItem(item, download_file, DownloadItem::IN_PROGRESS);
+}
+
TEST_F(DownloadItemTest, CopyDownload) {
DownloadItemImpl* item = CreateDownloadItem();
MockDownloadFile* download_file =
Regression Test / PoC
diff --git a/components/download/internal/common/download_item_impl_unittest.cc b/components/download/internal/common/download_item_impl_unittest.cc
index e368d7e..bd52c17 100644
--- a/components/download/internal/common/download_item_impl_unittest.cc
+++ b/components/download/internal/common/download_item_impl_unittest.cc
@@ -27,6 +27,7 @@
#include "base/task/single_thread_task_runner.h"
#include "base/test/gmock_move_support.h"
#include "base/test/metrics/histogram_tester.h"
+#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/threading/thread.h"
@@ -2015,6 +2016,37 @@
EXPECT_EQ(DownloadItem::COMPLETE, item->GetState());
}
+// The default DownloadItemImplDelegate (used when no embedder-level delegate
+// is attached, e.g. by InProgressDownloadManager) must defer completion of a
+// download whose content check is still pending so the file is not renamed to
+// its final path before the check resolves.
+TEST_F(DownloadItemTest,
+ DefaultDelegateDefersCompletionForPendingContentCheck) {
+ DownloadItemImpl* item = CreateDownloadItem();
+ MockDownloadFile* download_file =
+ DoIntermediateRename(item, DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT);
+ EXPECT_FALSE(item->IsDangerous());
+ EXPECT_EQ(DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT,
+ item->GetDangerType());
+
+ EXPECT_CALL(*mock_delegate(), ShouldCompleteDownload_(item, _))
+ .WillRepeatedly([&](DownloadItemImpl* download, base::OnceClosure& cb) {
+ return mock_delegate()
+ ->DownloadItemImplDelegate::ShouldCompleteDownload(download,
+ std::move(cb));
+ });
+ EXPECT_CALL(*download_file, RenameAndAnnotate(_, _, _, _, _, _, _)).Times(0);
+ item->DestinationObserverAsWeakPtr()->DestinationCompleted(
+ 0, std::unique_ptr<crypto::SecureHash>());
+ ASSERT_TRUE(base::test::RunUntil([&]() { return item->AllDataSaved(); }));
+
+ EXPECT_TRUE(item->AllDataSaved());
+ EXPECT_EQ(DownloadItem::IN_PROGRESS, item->GetState());
+ EXPECT_EQ(DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT,
+ item->GetDangerType());
+ CleanupItem(item, download_file, DownloadItem::IN_PROGRESS);
+}
+
TEST_F(DownloadItemTest, CopyDownload) {
DownloadItemImpl* item = CreateDownloadItem();
MockDownloadFile* download_file =
Original Bug Report
Potential Safe Browsing bypass via Android minimal-mode background resumption
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A potential security vulnerability exists in Chrome for Android where Safe Browsing content scans can be bypassed by exploiting background download resumption in minimal browser mode. When an interrupted download is resumed and completed by the background task, the InProgressDownloadManager fails to perform the mandatory ShouldCompleteDownload check, allowing a potentially dangerous file to be finalized without a scan.
Affected files:
components/download/internal/common/in_progress_download_manager.cccomponents/download/internal/common/download_item_impl_delegate.cccomponents/download/internal/common/download_item_impl.ccchrome/android/java/src/org/chromium/chrome/browser/download/service/DownloadBackgroundTask.javachrome/browser/download/android/download_manager_service.cc
Estimated timestamp from git blame: 2023-11-09
Summary
A potential vulnerability exists in Chrome for Android where Safe Browsing protection can be bypassed for downloads that are resumed via the DownloadBackgroundTask in minimal browser mode. In this mode, the InProgressDownloadManager acts as the DownloadItemImplDelegate but fails to implement the ShouldCompleteDownload check that triggers Safe Browsing content scans. Consequently, malicious files can be downloaded and saved to the user’s device without undergoing a content scan, even if they were originally flagged for scanning.
Technical Details
When a download is initiated in the full browser, ChromeDownloadManagerDelegate determines if a Safe Browsing scan is needed. If so, it sets the danger_type to DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT. In the full browser, ChromeDownloadManagerDelegate::ShouldCompleteDownload blocks completion until the Safe Browsing verdict is received.
On Android, Chrome can be terminated (e.g., due to memory pressure), and interrupted downloads are resumed by the DownloadBackgroundTask. This task often runs Chrome in “minimal browser mode” (supportsMinimalBrowser() returns true). In this mode, the full content::DownloadManager (and the ChromeDownloadManagerDelegate which normally enforces Safe Browsing checks) is not initialized because RunBrowserProcessMain is skipped.
Instead, InProgressDownloadManager is used as the delegate for restored downloads. InProgressDownloadManager does not override ShouldCompleteDownload. It inherits the base-class implementation in DownloadItemImplDelegate, which returns true unconditionally. Furthermore, DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT returns false for IsDangerous(), making these downloads eligible for auto-resumption and allowing them to bypass the IsDownloadReadyForCompletion() danger check.
As a result, when the background task resumes and completes the download, MaybeCompleteDownload() calls ShouldCompleteDownload(), receives true immediately, and the file is moved to its final path without the Safe Browsing content check ever being performed.
Potential Reproduction Steps
(Note: These are potential steps based on code analysis; our tooling agent doesn’t yet have the ability to run code to verify them.)
- Use an Android device and initiate a download for a file type that triggers a Safe Browsing content scan (e.g., an APK or executable) from an attacker-controlled server.
- Accept any initial download prompt. Chrome sets the
danger_typetoMAYBE_DANGEROUS_CONTENTand persists the state to the in-progress database. - The attacker server intentionally drops the connection midway through the download.
- Ensure the Chrome process is terminated (e.g., by opening other apps to trigger an OOM kill or using developer tools to stop the process).
- Wait for the Android
JobSchedulerto fire theDownloadBackgroundTask. This task will start Chrome in minimal mode. - The background task identifies the resumable download and calls
Resume(). The attacker server serves the full malicious payload upon resumption. - Once all data is saved,
MaybeCompleteDownloadis called. Because the delegate isInProgressDownloadManager(which lacks aShouldCompleteDownloadoverride), it returnstrueimmediately without checking with Safe Browsing. - The file is renamed to its final path in the Downloads folder, bypassing the content scan.
Suggested Fix
Implement ShouldCompleteDownload in InProgressDownloadManager. If a download has a danger_type indicating it needs a Safe Browsing scan (such as DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT), the manager should either:
- Delay completion and trigger the initialization of the full browser process so that
ChromeDownloadManagerDelegatecan perform the scan, OR - Block the completion and mark the download as interrupted or failed if the scan cannot be performed in minimal mode.
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.