Chrome · Downloads
CVE-2025-13634
Logic Error in Downloads
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
forchrome/browser/download/save_page_browsertest.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/download/save_page_browsertest.cc |
modified | |
ifcontent/browser/download/save_file_manager.cc |
modified |
Files Changed
chrome/browser/download/save_page_browsertest.cccontent/browser/download/save_file.cccontent/browser/download/save_file.hcontent/browser/download/save_file_manager.cc
Patch
From ade24699eeba445f5c671a73103f60442a1539ed Mon Sep 17 00:00:00 2001 From: Min Qin <[email protected]> Date: Wed, 22 Oct 2025 11:39:53 -0700 Subject: [PATCH] Run quarantine for all file save items Currently, quarantine is only run for files saved from the network. This CL make Chrome to also run quarantine for files that is saved from DOM since the DOM content is also from the web. This CL adds a quarantine callback to each SaveFile, so when SaveFile is finished, the quarantine will be invoked before completing the SavePackage. Bug: 429140219 Change-Id: Id1b2891a4ed48a17b77f83f0de671e8baa6e5159 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7046052 Reviewed-by: Shakti Sahu <[email protected]> Commit-Queue: Min Qin <[email protected]> Cr-Commit-Position: refs/heads/main@{#1533793} --- diff --git a/chrome/browser/download/save_page_browsertest.cc b/chrome/browser/download/save_page_browsertest.cc index a2385a2..e038e24 100644 --- a/chrome/browser/download/save_page_browsertest.cc +++ b/chrome/browser/download/save_page_browsertest.cc @@ -69,6 +69,7 @@ #include "ui/shell_dialogs/fake_select_file_dialog.h" #if BUILDFLAG(IS_CHROMEOS) +#include "base/test/test_future.h" #include "chromeos/dbus/dlp/dlp_client.h" #endif // BUILDFLAG(IS_CHROMEOS) @@ -1712,27 +1713,40 @@ chromeos::DlpClient::Shutdown(); chromeos::DlpClient::InitializeFake(); - base::test::RepeatingTestFuture< - dlp::AddFilesRequest, base::OnceCallback<void(dlp::AddFilesResponse)>> - add_file_cb; - chromeos::DlpClient::Get()->GetTestInterface()->SetAddFilesMock( - add_file_cb.GetCallback()); - url = NavigateToMockURL("a"); + // Use page "b" which has subresources (1.png, 1.css). Total 3 files. + url = NavigateToMockURL("b"); - SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML, "a", 1, &dir, + SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML, "b", 3, &dir, &full_file_name); ASSERT_FALSE(HasFailure()); - auto request = std::get<0>(add_file_cb.Take()); - ASSERT_EQ(1, request.add_file_requests().size()); - EXPECT_EQ(full_file_name.value(), request.add_file_requests(0).file_path()); - EXPECT_EQ(request.add_file_requests(0).source_url(), url.spec()); + // Asynchronously get the recorded requests from the fake client. + base::test::TestFuture<const dlp::GetDatabaseEntriesResponse> future; + chromeos::DlpClient::Get()->GetDatabaseEntries(future.GetCallback()); + const auto& response = future.Get(); + const auto& requests = response.files_entries(); - base::ScopedAllowBlockingForTesting allow_blocking; - EXPECT_TRUE(base::PathExists(full_file_name)); - EXPECT_FALSE(base::PathExists(dir)); + // There is a total of 6 requests, 3 for temporary files and 3 for final + // destination. + ASSERT_EQ(6, requests.size()); + + // The order of subresource saving is not guaranteed, so we use a set + // to verify the presence of each expected file path. + std::set<std::string> expected_paths; + expected_paths.insert(full_file_name.value()); + expected_paths.insert(dir.AppendASCII("1.png").value()); + expected_paths.insert(dir.AppendASCII("1.css").value()); + + std::set<std::string> actual_paths; + for (const auto& request : requests) { + actual_paths.insert(request.path()); + } + + for (const auto& expected_path : expected_paths) { + EXPECT_TRUE(base::Contains(actual_paths, expected_path)); + } } IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, SaveMHTMLWithDlp) { diff --git a/content/browser/download/save_file.cc b/content/browser/download/save_file.cc index 6eed86d5..89dfc43 100644 --- a/content/browser/download/save_file.cc +++ b/content/browser/download/save_file.cc @@ -91,4 +91,10 @@ return file_.DebugString(); } +void SaveFile::RunQuarantineCallback() { + if (!info_->quarantine_callback.is_null()) { + std::move(info_->quarantine_callback).Run(); + } +} + } // namespace content diff --git a/content/browser/download/save_file.h b/content/browser/download/save_file.h index 77dde846..71497ef 100644 --- a/content/browser/download/save_file.h +++ b/content/browser/download/save_file.h @@ -56,6 +56,8 @@ } const SaveFileCreateInfo& create_info() const { return *info_; } + void RunQuarantineCallback(); + private: download::BaseFile file_; std::unique_ptr<SaveFileCreateInfo> info_; diff --git a/content/browser/download/save_file_manager.cc b/content/browser/download/save_file_manager.cc index 41f27959..5e1057c 100644 --- a/content/browser/download/save_file_manager.cc +++ b/content/browser/download/save_file_manager.cc @@ -71,11 +71,13 @@ const net::NetworkTrafficAnnotationTag& annotation_tag, network::mojom::URLLoaderFactory* url_loader_factory, SaveFileManager* save_file_manager, + base::OnceClosure quarantine_callback, URLLoaderCompleteCallback on_complete_cb) { return std::unique_ptr<SimpleURLLoaderHelper>(new SimpleURLLoaderHelper( std::move(resource_request), save_item_id, save_package_id, render_process_id, render_frame_routing_id, annotation_tag, - url_loader_factory, save_file_manager, std::move(on_complete_cb))); + url_loader_factory, save_file_manager, std::move(quarantine_callback), + std::move(on_complete_cb))); } SimpleURLLoaderHelper(const SimpleURLLoaderHelper&) = delete; @@ -93,10 +95,12 @@ const net::NetworkTrafficAnnotationTag& annotation_tag, network::mojom::URLLoaderFactory* url_loader_factory, SaveFileManager* save_file_manager, + base::OnceClosure quarantine_callback, URLLoaderCompleteCallback on_complete_cb) : save_file_manager_(save_file_manager), save_item_id_(save_item_id), save_package_id_(save_package_id), + quarantine_callback_(std::move(quarantine_callback)), on_complete_cb_(std::move(on_complete_cb)) { GURL url = resource_request->url; url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request), @@ -124,6 +128,7 @@ auto info = std::make_unique<SaveFileCreateInfo>( url, final_url, save_item_id_, save_package_id_, render_process_id, render_frame_routing_id, content_disposition); + info->quarantine_callback = std::move(quarantine_callback_); download::GetDownloadTaskRunner()->PostTask( FROM_HERE, base::BindOnce(&SaveFileManager::StartSave, save_file_manager_, std::move(info))); @@ -155,6 +160,7 @@ SaveItemId save_item_id_; SavePackageId save_package_id_; std::unique_ptr<network::SimpleURLLoader> url_loader_; + base::OnceClosure quarantine_callback_; URLLoaderCompleteCallback on_complete_cb_; }; @@ -227,6 +233,12 @@ DCHECK(!base::Contains(packages_, save_item_id)); packages_[save_item_id] = save_package; + base::OnceClosure quarantine_callback = base::BindOnce( + &SaveFileManager::QuarantineItem, this, save_item_id, save_package->id(), + context->IsOffTheRecord() ? GURL() : url, + context->IsOffTheRecord() ? GURL() : referrer.url, client_guid, + std::move(remote_quarantine)); + // Register a saving job. if (save_source == SaveFileCreateInfo::SAVE_FILE_FROM_NET) { DCHECK(url.is_valid()); @@ -317,22 +329,20 @@ } base::OnceCallback<void(bool /*success*/)> save_finished_cb = - base::BindOnce(&SaveFileManager::OnURLLoaderComplete, this, - save_item_id, save_package->id(), - context->IsOffTheRecord() ? GURL() : url, - context->IsOffTheRecord() ? GURL() : referrer.url, - client_guid, std::move(remote_quarantine)); - + base::BindOnce(&SaveFileManager::SaveFinished, this, save_item_id, + save_package->id()); url_loader_helpers_[save_item_id] = SimpleURLLoaderHelper::CreateAndStartDownload( std::move(request), save_item_id, save_package->id(), render_process_host_id, render_frame_routing_id, traffic_annotation, - factory, this, std::move(save_finished_cb)); + factory, this, std::move(quarantine_callback), + std::move(save_finished_cb)); } else { // We manually start the save job. auto info = std::make_unique<SaveFileCreateInfo>( file_full_path, url, save_item_id, save_package->id(), render_process_host_id, render_frame_routing_id, save_source);
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/download/save_page_browsertest.cc b/chrome/browser/download/save_page_browsertest.cc
index a2385a2..e038e24 100644
--- a/chrome/browser/download/save_page_browsertest.cc
+++ b/chrome/browser/download/save_page_browsertest.cc
@@ -69,6 +69,7 @@
#include "ui/shell_dialogs/fake_select_file_dialog.h"
#if BUILDFLAG(IS_CHROMEOS)
+#include "base/test/test_future.h"
#include "chromeos/dbus/dlp/dlp_client.h"
#endif // BUILDFLAG(IS_CHROMEOS)
@@ -1712,27 +1713,40 @@
chromeos::DlpClient::Shutdown();
chromeos::DlpClient::InitializeFake();
- base::test::RepeatingTestFuture<
- dlp::AddFilesRequest, base::OnceCallback<void(dlp::AddFilesResponse)>>
- add_file_cb;
- chromeos::DlpClient::Get()->GetTestInterface()->SetAddFilesMock(
- add_file_cb.GetCallback());
- url = NavigateToMockURL("a");
+ // Use page "b" which has subresources (1.png, 1.css). Total 3 files.
+ url = NavigateToMockURL("b");
- SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML, "a", 1, &dir,
+ SaveCurrentTab(url, content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML, "b", 3, &dir,
&full_file_name);
ASSERT_FALSE(HasFailure());
- auto request = std::get<0>(add_file_cb.Take());
- ASSERT_EQ(1, request.add_file_requests().size());
- EXPECT_EQ(full_file_name.value(), request.add_file_requests(0).file_path());
- EXPECT_EQ(request.add_file_requests(0).source_url(), url.spec());
+ // Asynchronously get the recorded requests from the fake client.
+ base::test::TestFuture<const dlp::GetDatabaseEntriesResponse> future;
+ chromeos::DlpClient::Get()->GetDatabaseEntries(future.GetCallback());
+ const auto& response = future.Get();
+ const auto& requests = response.files_entries();
- base::ScopedAllowBlockingForTesting allow_blocking;
- EXPECT_TRUE(base::PathExists(full_file_name));
- EXPECT_FALSE(base::PathExists(dir));
+ // There is a total of 6 requests, 3 for temporary files and 3 for final
+ // destination.
+ ASSERT_EQ(6, requests.size());
+
+ // The order of subresource saving is not guaranteed, so we use a set
+ // to verify the presence of each expected file path.
+ std::set<std::string> expected_paths;
+ expected_paths.insert(full_file_name.value());
+ expected_paths.insert(dir.AppendASCII("1.png").value());
+ expected_paths.insert(dir.AppendASCII("1.css").value());
+
+ std::set<std::string> actual_paths;
+ for (const auto& request : requests) {
+ actual_paths.insert(request.path());
+ }
+
+ for (const auto& expected_path : expected_paths) {
+ EXPECT_TRUE(base::Contains(actual_paths, expected_path));
+ }
}
IN_PROC_BROWSER_TEST_F(SavePageBrowserTest, SaveMHTMLWithDlp) {
Loading diff…
Original Bug Report
reported by [email protected]
Zone.Identifier MotW is missing for files saved with Ctrl+S
Steps to reproduce the problem
- Visit e.g. https://bayden.com/echo.aspx
- Hit CTRL+S to save the file
- Save the file using “Web Page, Complete” format.
OBSERVE: File does not have a Mark-of-the-Web alternate data stream.
Problem Description
The lack of a Zone.identifier based MotW was documented by https://mrd0x.com/filefix-part-2/ in an attack chain that involves tricking the user into saving a HTA file.
This appears to be a regression since https://issues.chromium.org/issues/40082670#comment6
If the user chooses “Web Page, HTML Only” the Zone.Identifier MotW is attached correctly. It’s only “Web Page, Complete” that doesn’t attach one.
Summary
Zone.Identifier MotW is missing for files saved with Ctrl+S
Additional Data
Category: Security
Chrome Channel: Not sure
Regression: Yes \
References
On This Page