Medium watch CVSS 6.5 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Downloads
DescriptionInappropriate implementation in Downloads
ComponentDownloads
Sub-componentDownloads
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker497986036
Fix commit79649f9623c5 (chromium/src) +43/-1
CWECWE-841
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-31 (per vendor advisory)
Fix landed2026-07-30 (public git)
Disclosed2026-09-08
Patch gap40 days — fix public in source before the advisory

Background

`DownloadItemImplDelegate::ShouldCompleteDownload()`
the gate DownloadItemImpl consults before renaming an in-progress download from its intermediate path to its final path; the base implementation is used when no richer embedder delegate is attached.
Content-check-pending danger types
the enum values DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT, DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING, and DOWNLOAD_DANGER_TYPE_ASYNC_LOCAL_PASSWORD_SCANNING, which mark a download whose asynchronous verdict has not yet resolved.
`InProgressDownloadManager`
the minimal (non-full-browser) download owner, on Android especially, that inherits the base DownloadItemImplDelegate stub rather than the full DownloadManagerImpl override.
`IsDangerous()`
the predicate that returns false for DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT, so a pending-verdict download is not treated as “dangerous” and is not held back on that basis alone.

Root Cause Analysis

The base DownloadItemImplDelegate::ShouldCompleteDownload() returned true unconditionally, telling DownloadItemImpl that a download was ready to be finalized regardless of its danger type. When InProgressDownloadManager (which inherits this stub) owned a resumed download in minimal-browser mode with danger type DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT, the unconditional true let the item proceed to RenameAndAnnotate() — moving the file to its final path — before any content check had run, as stated in the commit message. Because that danger type returns false from IsDangerous(), no other guard held the download back, so the “do not finalize until the content verdict resolves” invariant was silently violated.

The fix replaces the unconditional return true with a switch on download->GetDangerType() that returns false for the three content-check-pending danger types and true otherwise, keeping such downloads in IN_PROGRESS until a delegate that can actually run the check takes over. Because DownloadManagerImpl already overrides ShouldCompleteDownload(), full-browser behavior is unchanged and only the minimal-browser path is corrected.

Key insight
The single mistake was that the default delegate’s gate blindly approved completion (return true) even for downloads whose safety verdict was still pending; the fix teaches the gate to defer (return false) precisely those content-check-pending danger types so the file is never renamed to its final path ahead of its check.

Attack Path

  1. Reach the minimal-browser download owner An attacker gets a victim on Android to download content handled by InProgressDownloadManager (minimal-browser mode), which inherits the base DownloadItemImplDelegate rather than DownloadManagerImpl.
  2. Land in a pending-verdict state The download is assigned danger type DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT (content check not yet resolved), a state for which IsDangerous() returns false, so no danger-based guard holds it back.
  3. Resume to trigger completion The download is resumed and its data completes, driving DownloadItemImpl to consult ShouldCompleteDownload(), which pre-patch returned true unconditionally.
  4. File finalized before the check With completion approved, the item reaches RenameAndAnnotate() and the file is renamed to its final path before the content check runs, so a file that a pending scan might have flagged is committed to its destination anyway (the diff only adds a deferral check; it does not itself demonstrate any further exploitation).
  5. Downstream execution of unscanned content (inferred) — A file placed at its final path without its content verdict could then be opened/executed by the user or another component as if vetted; the patch does not demonstrate this.

Impact Assessment

An attacker gains the ability to have a not-yet-scanned download committed to its final on-disk path in the browser process before its content check resolves, in Android minimal-browser (InProgressDownloadManager) contexts where the base DownloadItemImplDelegate is used; the precondition is a resumed download carrying a content-check-pending danger type such as DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT. This is a logic error (inappropriate implementation) that weakens the download safety-check ordering rather than a memory-corruption primitive. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 6.5. The advisory reports no exploited-in-the-wild status or severity beyond the “Inappropriate implementation in Downloads” classification.

Changed Functions

FunctionChangeNotes
ASSERT_TRUE
components/download/internal/common/download_item_impl_unittest.cc
modified
TEST_F
components/download/internal/common/download_item_impl_unittest.cc
modified

Files Changed

  • components/download/internal/common/download_item_impl_unittest.cc
  • components/download/internal/common/download_item_impl_delegate.cc

Audit Directions

  • Unconditional base/default delegate stubs
    Look for base-class or default-delegate methods that return true/approve an action unconditionally where a subclass (e.g. DownloadManagerImpl) implements the real safety logic, since minimal-mode owners inheriting the stub silently bypass the check.
  • Danger-type gates that trust `IsDangerous()`
    Audit completion, rename, and open paths that rely on IsDangerous() alone, since pending-verdict types like DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT return false and can slip past danger-only guards while their async verdict is still outstanding.
  • Async-verdict ordering on resume/minimal-browser paths
    Trace resumed-download and InProgressDownloadManager flows to confirm no state transition (especially RenameAndAnnotate() / finalization) can run before every pending async scan (ASYNC_SCANNING, ASYNC_LOCAL_PASSWORD_SCANNING) has resolved.
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 =
Loading diff…

Regression Test / PoC

shipped with the fix
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 =
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • components/download/internal/common/download_item_impl_delegate.cc
  • components/download/internal/common/download_item_impl.cc
  • chrome/android/java/src/org/chromium/chrome/browser/download/service/DownloadBackgroundTask.java
  • chrome/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.)

  1. 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.
  2. Accept any initial download prompt. Chrome sets the danger_type to MAYBE_DANGEROUS_CONTENT and persists the state to the in-progress database.
  3. The attacker server intentionally drops the connection midway through the download.
  4. 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).
  5. Wait for the Android JobScheduler to fire the DownloadBackgroundTask. This task will start Chrome in minimal mode.
  6. The background task identifies the resumable download and calls Resume(). The attacker server serves the full malicious payload upon resumption.
  7. Once all data is saved, MaybeCompleteDownload is called. Because the delegate is InProgressDownloadManager (which lacks a ShouldCompleteDownload override), it returns true immediately without checking with Safe Browsing.
  8. 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:

  1. Delay completion and trigger the initialization of the full browser process so that ChromeDownloadManagerDelegate can perform the scan, OR
  2. 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.

View on issue tracker