Low watch CVSS 5.4 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Downloads
DescriptionInappropriate implementation in Downloads
ComponentDownloads
Sub-componentUI>Browser>Downloads
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.
Tracker431309019
Fix commitcbd95c7b79a5 (chromium/src) +40/-2
CWECWE-346 (Origin validation error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedTom Haas
Reported2025-07-12 · bounty $2000 (per vendor advisory)
Fix landed2025-07-29 (public git)
Disclosed2025-09-02
Patch gap35 days — fix public in source before the advisory

Background

`DownloadTargetDeterminer`
the browser-side state machine that picks the final on-disk target path for a download and then schedules a callback with the result.
`PathValidationResult`
the enum returned by path reservation describing the outcome, including SUCCESS, SUCCESS_RESOLVED_CONFLICT, and SAME_AS_SOURCE (the chosen target equals the download’s source path).
`ScheduleCallbackAndDeleteSelf`
the determiner’s terminal step that reports a result (optionally an interrupt reason) and destroys the determiner.
`DOWNLOAD_INTERRUPT_REASON_FILE_SAME_AS_SOURCE`
the interrupt reason indicating the download must not proceed because target and source coincide.

Root Cause Analysis

Two switch (result) statements in download_target_determiner.cc handled PathValidationResult::SAME_AS_SOURCE by treating it like a success: the first grouped it with SUCCESS/SUCCESS_RESOLVED_CONFLICT and merely DCHECKed then break, and the second grouped it with SUCCESS and break, so determination continued as if the target were valid. As the commit notes, the original intent (from CL 475792) was to disallow a download whose target equals its source, but that code never actually cancelled or interrupted the download. The consequence, per the commit, is that Chrome could enter a download loop — auto-open a binary file, which produces a download to the same path, which auto-opens again — because nothing stopped the same-as-source case.

The fix splits SAME_AS_SOURCE into its own case in both switches, calling ScheduleCallbackAndDeleteSelf(DOWNLOAD_INTERRUPT_REASON_FILE_SAME_AS_SOURCE) and returning, so the download is now interrupted rather than allowed to complete.

Key insight
The core mistake was that SAME_AS_SOURCE was folded into the success branches and only asserted/broken instead of being acted on, so the long-standing intent to disallow such downloads was never enforced; the fix gives it a dedicated case that interrupts and tears down the determiner.

Attack Path

  1. Point a download at its own source A page initiates a download whose computed target path resolves to the same file as the source (the test uses a file:// URL via net::FilePathToFileURL, so target equals source), producing PathValidationResult::SAME_AS_SOURCE.
  2. Pass the success-grouped switch Pre-patch, SAME_AS_SOURCE fell into the SUCCESS grouping and merely breaks, so DownloadTargetDeterminer proceeds instead of interrupting.
  3. Complete/auto-open the file The download is treated as valid; for an auto-openable binary this triggers auto-open of the file at that path.
  4. Loop Per the commit, auto-opening re-produces a download to the same target, which again validates as SAME_AS_SOURCE and again proceeds, leaving Chrome stuck in a repeating download/auto-open loop; any impact beyond this loop/inappropriate-download behavior is (inferred).

Impact Assessment

An attacker can cause a download to proceed (and potentially auto-open in a loop) when its target equals its source, an inappropriate-implementation flaw in the Downloads component running in the browser process, rather than a memory-safety primitive. The precondition is the ability to initiate a download that resolves to a same-as-source path (e.g. via a file:// target). The advisory classifies this as LogicError / “Inappropriate implementation in Downloads” and does not report exploitation in the wild; any user-harm escalation is (inferred).

Changed Functions

FunctionChangeNotes
switch
chrome/browser/download/download_target_determiner.cc
modified
TEST_F
chrome/browser/download/download_target_determiner_unittest.cc
modified

Files Changed

  • chrome/browser/download/download_target_determiner.cc
  • chrome/browser/download/download_target_determiner_unittest.cc

Audit Directions

  • Error/edge enum values grouped with success
    Audit switch statements where a distinct result value (like SAME_AS_SOURCE) shares a case with success and only breaks/DCHECKs, silently allowing a path that was meant to be rejected.
  • Stated-intent vs. enforcement gaps
    Where comments/history claim a condition is “disallowed,” verify code actually cancels/interrupts rather than merely continuing, as here.
  • Self-referential download/open loops
    Review target-determination and auto-open logic for cases where the produced file can re-trigger the same download, especially for file:// and same-path targets.
From cbd95c7b79a57daf0d850ff4ebc3e9ebf44275ff Mon Sep 17 00:00:00 2001
From: Min Qin <qinmin@chromium.org>
Date: Tue, 29 Jul 2025 10:56:41 -0700
Subject: [PATCH] Cancel download if target is the same as source

The original comments about disallowing a download if target is the same
as the source is from here:
https://chromium-review.googlesource.com/c/chromium/src/+/475792.
However, this CL doesn't seem to cancel or interrupt the download.
So this CL will do the cancelling of the download. This may have
some implications on file URLs since the target filepath may always
be the same as the the original path. An alternative is to always
uniquify the file path in that case. However, this doesn't solve
the issue that Chrome may stuck in a download loop to try to
auto-open an binary file->result in a download->auto-open again...

Bug: 431309019
Change-Id: I06767b70a280b2e5780033058cf2c333be5740f9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6758511
Reviewed-by: David Trainor <dtrainor@chromium.org>
Commit-Queue: Min Qin <qinmin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1493653}
---

diff --git a/chrome/browser/download/download_target_determiner.cc b/chrome/browser/download/download_target_determiner.cc
index cae68c5..ecd90d18 100644
--- a/chrome/browser/download/download_target_determiner.cc
+++ b/chrome/browser/download/download_target_determiner.cc
@@ -500,10 +500,13 @@
         return;
       case download::PathValidationResult::SUCCESS:
       case download::PathValidationResult::SUCCESS_RESOLVED_CONFLICT:
-      case download::PathValidationResult::SAME_AS_SOURCE:
         DCHECK(virtual_path_ == path ||
                conflict_action_ == DownloadPathReservationTracker::UNIQUIFY);
         break;
+      case download::PathValidationResult::SAME_AS_SOURCE:
+        ScheduleCallbackAndDeleteSelf(
+            download::DOWNLOAD_INTERRUPT_REASON_FILE_SAME_AS_SOURCE);
+        return;
       case download::PathValidationResult::COUNT:
         NOTREACHED();
     }
@@ -512,9 +515,13 @@
 
     switch (result) {
       case download::PathValidationResult::SUCCESS:
-      case download::PathValidationResult::SAME_AS_SOURCE:
         break;
 
+      case download::PathValidationResult::SAME_AS_SOURCE:
+        ScheduleCallbackAndDeleteSelf(
+            download::DOWNLOAD_INTERRUPT_REASON_FILE_SAME_AS_SOURCE);
+        return;
+
       // TODO(crbug.com/40863725): This should trigger a duplicate download
       // prompt.
       case download::PathValidationResult::SUCCESS_RESOLVED_CONFLICT:
diff --git a/chrome/browser/download/download_target_determiner_unittest.cc b/chrome/browser/download/download_target_determiner_unittest.cc
index 002ec88d..ad7af00 100644
--- a/chrome/browser/download/download_target_determiner_unittest.cc
+++ b/chrome/browser/download/download_target_determiner_unittest.cc
@@ -64,6 +64,7 @@
 #include "content/public/test/web_contents_tester.h"
 #include "extensions/buildflags/buildflags.h"
 #include "google_apis/gaia/gaia_id.h"
+#include "net/base/filename_util.h"
 #include "net/base/mime_util.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -2657,6 +2658,36 @@
   histogram_tester.ExpectTotalCount(kTransientPathValidationHistogram, 1);
 }
 
+TEST_F(DownloadTargetDeterminerTest, TargetSameAsSource) {
+  const base::FilePath::CharType kInitialPath[] =
+      FILE_PATH_LITERAL("download.txt");
+  base::FilePath expected_path = GetPathInDownloadDir(kInitialPath);
+  GURL file_url = net::FilePathToFileURL(expected_path);
+
+  const DownloadTestCase kTestCase = {
+      AUTOMATIC,
+      download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
+      DownloadFileType::NOT_DANGEROUS,
+      file_url.spec().c_str(),
+      "text/plain",
+      FILE_PATH_LITERAL(""),
+      FILE_PATH_LITERAL(""),
+      DownloadItem::TARGET_DISPOSITION_OVERWRITE,
+      EXPECT_EMPTY};
+
+  const DownloadTestCase& test_case = kTestCase;
+  std::unique_ptr<download::MockDownloadItem> item =
+      CreateActiveDownloadItem(0, test_case);
+
+  EXPECT_CALL(*delegate(),
+              ReserveVirtualPath_(_, expected_path, false,
+                                  DownloadPathReservationTracker::UNIQUIFY, _))
+      .WillOnce(WithArg<4>(ScheduleCallback2(
+          download::PathValidationResult::SAME_AS_SOURCE, expected_path)));
+
+  RunTestCase(test_case, expected_path, item.get());
+}
+
 #if BUILDFLAG(IS_WIN)
 // Test that env variables will be removed from file name before prompting Save
 // As dialog.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/download/download_target_determiner_unittest.cc b/chrome/browser/download/download_target_determiner_unittest.cc
index 002ec88d..ad7af00 100644
--- a/chrome/browser/download/download_target_determiner_unittest.cc
+++ b/chrome/browser/download/download_target_determiner_unittest.cc
@@ -64,6 +64,7 @@
 #include "content/public/test/web_contents_tester.h"
 #include "extensions/buildflags/buildflags.h"
 #include "google_apis/gaia/gaia_id.h"
+#include "net/base/filename_util.h"
 #include "net/base/mime_util.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -2657,6 +2658,36 @@
   histogram_tester.ExpectTotalCount(kTransientPathValidationHistogram, 1);
 }
 
+TEST_F(DownloadTargetDeterminerTest, TargetSameAsSource) {
+  const base::FilePath::CharType kInitialPath[] =
+      FILE_PATH_LITERAL("download.txt");
+  base::FilePath expected_path = GetPathInDownloadDir(kInitialPath);
+  GURL file_url = net::FilePathToFileURL(expected_path);
+
+  const DownloadTestCase kTestCase = {
+      AUTOMATIC,
+      download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
+      DownloadFileType::NOT_DANGEROUS,
+      file_url.spec().c_str(),
+      "text/plain",
+      FILE_PATH_LITERAL(""),
+      FILE_PATH_LITERAL(""),
+      DownloadItem::TARGET_DISPOSITION_OVERWRITE,
+      EXPECT_EMPTY};
+
+  const DownloadTestCase& test_case = kTestCase;
+  std::unique_ptr<download::MockDownloadItem> item =
+      CreateActiveDownloadItem(0, test_case);
+
+  EXPECT_CALL(*delegate(),
+              ReserveVirtualPath_(_, expected_path, false,
+                                  DownloadPathReservationTracker::UNIQUIFY, _))
+      .WillOnce(WithArg<4>(ScheduleCallback2(
+          download::PathValidationResult::SAME_AS_SOURCE, expected_path)));
+
+  RunTestCase(test_case, expected_path, item.get());
+}
+
 #if BUILDFLAG(IS_WIN)
 // Test that env variables will be removed from file name before prompting Save
 // As dialog.
Loading diff…

Original Bug Report

reported by to...@gmail.com

Bypassing Mark of the Web with an HTML File and User Interaction


Report description

Bypassing Mark of the Web with an HTML File and User Interaction


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules


The problem

Please describe the technical details of the vulnerability

Vulnerability

A locally opened HTML file can be used to remove the Mark of the Web from any file accessible to the targeted user, including attacker-supplied executables.

How?

Depending on whether a download is initiated by user interaction or generated via a data-src URL, the file will have different HostUrl values and ZoneIds. Regardless of the ZoneID, the Mark of the Web can be fully removed simply by referencing the downloaded executable using its complete local path. This is only possible if the HTML file is opened locally via file:///.

PoC

By referencing the downloaded file with its full path, for example <iframe src="file:///C:/Users/<user>/Downloads/calc.exe">, the browser will attempt to download the file again if it is an executable or a non-displayable file (such as a ZIP archive), instead of rendering its contents.

Instead of creating a new copy, the browser appears to overwrite the existing file, incorrectly treating it as a trusted local file. As a result, the Mark of the Web is removed from the payload.

You can retrieve the username in Chrome (with default settings) by reading window.location.href and extracting it with match(/file:\/\/\/[a-zA-Z]:\/Users\/([^\/;]+)/i);.

Actual Bug that happens in background:

In Chrome, when downloading a file that already exists in the Downloads folder (referenced via its full path), the browser “overwrites” the original file instead of creating a copy. This overwriting removes the ADR (Alternate Data Stream) and, with it, the Mark of the Web.

Whether it really fully replaces the file with its full content or it just “strips” the ADR I am not sure of.

I believe, since the file is loaded via file:///C:... it is wrongly assumed as local origin. However, even if that were true, it should then, instead of omitting the ADR entirely, assign ZoneID 0.

More importantly, the ADR should be preserved: since the file originated from the internet and had MotW set, redownloading or referencing it locally should not strip the MotW.

Firefox Firefox does not overwrite the file, so the MotW is not removed. Therefore, this method does not work in Firefox.

Impact analysis – Please briefly explain who can exploit the vulnerability, and what they gain when doing so

What is the Impact?

Security Feature Bypass – Mark of the Web

This vulnerability enables attackers to distribute an HTML file containing an embedded executable payload. When a victim opens the HTML file and downloads the payload — either by clicking a link or through a delayed background download that avoids triggering the usual multiple-download permission popup — the payload can then be accessed and downloaded again using its full local file path. This second download strips the Mark of the Web (MotW) from the file. As a result, when the user is tricked into opening the file, no MotW is present and no security warning is displayed.

Exploiting this technique requires only basic technical skills.

For demonstration, two sample motw.html files are provided (one for ZoneId4 and one for ZoneId3, which triggers classic SmartScreen behavior), along with source code for a small executable, a template, and a PowerShell script to generate the motw.html file with the embedded executable (calc.c in ./src). The executable simply launches calc.exe. The two HTML scenarios showcase different approaches: one leads to ZoneID 4, and the other to ZoneID 3. These files do not need to be hosted online; Chrome will automatically apply the Mark of the Web to any file downloaded via a local HTML file.


The cause

Choose the type of vulnerability

Other

Does anyone else know about this vulnerability?

No, this vulnerability is private.

Do you plan to disclose this vulnerability publicly?

Yes, I plan to disclose

View on issue tracker