CVE-2025-12905
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchchrome/browser/download/download_target_determiner.cc |
modified | |
TEST_Fchrome/browser/download/download_target_determiner_unittest.cc |
modified |
Files Changed
chrome/browser/download/download_target_determiner.ccchrome/browser/download/download_target_determiner_unittest.cc
Patch
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.
Regression Test / PoC
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.
Original Bug Report
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