Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Downloads
DescriptionInappropriate implementation in Downloads
ComponentDownloads
Bug ClassLogic Error
Tracker511802911
Fix commit9c86d91a91f7 (chromium/src) +16/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
content/browser/download/save_file_manager.cc
modified

Files Changed

  • content/browser/download/save_file.cc
  • content/browser/download/save_file_manager.cc
  • content/browser/download/save_file_manager.h
  • content/browser/download/save_types.cc
  • content/browser/download/save_types.h
From 9c86d91a91f72145f4136079de16325a96cb1496 Mon Sep 17 00:00:00 2001
From: Min Qin <[email protected]>
Date: Thu, 21 May 2026 12:02:16 -0700
Subject: [PATCH] Use the destination URL after redirection for quarantine

This CL fixes an issue that quarantine checks the initial URL, instead
of the destination URL when saving a page.

Bug: 511802911
Change-Id: I1a3deed127add6db81fe20882303954807575cab
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7855782
Reviewed-by: Shakti Sahu <[email protected]>
Commit-Queue: Min Qin <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1634445}
---

diff --git a/content/browser/download/save_file.cc b/content/browser/download/save_file.cc
index 89dfc43..191c9c8b 100644
--- a/content/browser/download/save_file.cc
+++ b/content/browser/download/save_file.cc
@@ -93,7 +93,7 @@
 
 void SaveFile::RunQuarantineCallback() {
   if (!info_->quarantine_callback.is_null()) {
-    std::move(info_->quarantine_callback).Run();
+    std::move(info_->quarantine_callback).Run(info_->final_url);
   }
 }
 
diff --git a/content/browser/download/save_file_manager.cc b/content/browser/download/save_file_manager.cc
index 0b6f3ee2..9f642b46 100644
--- a/content/browser/download/save_file_manager.cc
+++ b/content/browser/download/save_file_manager.cc
@@ -70,7 +70,7 @@
       const net::NetworkTrafficAnnotationTag& annotation_tag,
       network::mojom::URLLoaderFactory* url_loader_factory,
       SaveFileManager* save_file_manager,
-      base::OnceClosure quarantine_callback,
+      base::OnceCallback<void(const GURL&)> quarantine_callback,
       URLLoaderCompleteCallback on_complete_cb) {
     return std::unique_ptr<SimpleURLLoaderHelper>(new SimpleURLLoaderHelper(
         std::move(resource_request), save_item_id, save_package_id,
@@ -94,7 +94,7 @@
       const net::NetworkTrafficAnnotationTag& annotation_tag,
       network::mojom::URLLoaderFactory* url_loader_factory,
       SaveFileManager* save_file_manager,
-      base::OnceClosure quarantine_callback,
+      base::OnceCallback<void(const GURL&)> quarantine_callback,
       URLLoaderCompleteCallback on_complete_cb)
       : save_file_manager_(save_file_manager),
         save_item_id_(save_item_id),
@@ -159,7 +159,7 @@
   SaveItemId save_item_id_;
   SavePackageId save_package_id_;
   std::unique_ptr<network::SimpleURLLoader> url_loader_;
-  base::OnceClosure quarantine_callback_;
+  base::OnceCallback<void(const GURL&)> quarantine_callback_;
   URLLoaderCompleteCallback on_complete_cb_;
 };
 
@@ -232,11 +232,10 @@
   DCHECK(!packages_.contains(save_item_id));
   packages_[save_item_id] = save_package;
 
-  base::OnceClosure quarantine_callback = base::BindOnce(
+  base::OnceCallback<void(const GURL&)> 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));
+      std::move(remote_quarantine), context->IsOffTheRecord());
 
   // Register a saving job.
   if (save_source == SaveFileCreateInfo::SAVE_FILE_FROM_NET) {
@@ -394,16 +393,18 @@
 void SaveFileManager::QuarantineItem(
     SaveItemId save_item_id,
     SavePackageId save_package_id,
-    const GURL& url,
     const GURL& referrer_url,
     const std::string& client_guid,
-    mojo::PendingRemote<quarantine::mojom::Quarantine> remote_quarantine) {
+    mojo::PendingRemote<quarantine::mojom::Quarantine> remote_quarantine,
+    bool is_off_the_record,
+    const GURL& url) {
   DCHECK(download::GetDownloadTaskRunner()->RunsTasksInCurrentSequence());
   SaveFile* save_file = LookupSaveFile(save_item_id);
   CHECK(save_file);
 
   save_file->AnnotateWithSourceInformation(
-      client_guid, url, referrer_url, std::move(remote_quarantine),
+      client_guid, is_off_the_record ? GURL() : url, referrer_url,
+      std::move(remote_quarantine),
       base::BindOnce(&SaveFileManager::OnQuarantineComplete, this, save_item_id,
                      save_package_id));
 }
diff --git a/content/browser/download/save_file_manager.h b/content/browser/download/save_file_manager.h
index 5297a0c..a245f93 100644
--- a/content/browser/download/save_file_manager.h
+++ b/content/browser/download/save_file_manager.h
@@ -194,10 +194,11 @@
   void QuarantineItem(
       SaveItemId save_item_id,
       SavePackageId save_package_id,
-      const GURL& url,
       const GURL& referrer_url,
       const std::string& client_guid,
-      mojo::PendingRemote<quarantine::mojom::Quarantine> remote_quarantine);
+      mojo::PendingRemote<quarantine::mojom::Quarantine> remote_quarantine,
+      bool is_off_the_record,
+      const GURL& url);
 
   // Called on the download TaskRunner when file quarantine finishes on a
   // SaveItem.
diff --git a/content/browser/download/save_types.cc b/content/browser/download/save_types.cc
index 1fdcb7a8..9aba511 100644
--- a/content/browser/download/save_types.cc
+++ b/content/browser/download/save_types.cc
@@ -17,6 +17,7 @@
                                        SaveFileSource save_source)
     : path(path),
       url(url),
+      final_url(url),
       save_item_id(save_item_id),
       save_package_id(save_package_id),
       render_process_id(render_process_id),
diff --git a/content/browser/download/save_types.h b/content/browser/download/save_types.h
index d1ce26c0..3dd7040 100644
--- a/content/browser/download/save_types.h
+++ b/content/browser/download/save_types.h
@@ -82,7 +82,7 @@
   // Source type of saved file.
   SaveFileSource save_source;
   // Callback to run to quarantine the file;
-  base::OnceClosure quarantine_callback;
+  base::OnceCallback<void(const GURL&)> quarantine_callback;
 };
 
 }  // namespace content
Loading diff…

Original Bug Report

reported by [email protected]

Potential SmartScreen bypass via Intranet MOTW spoofing in SavePackage

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The SavePackage feature incorrectly uses the initial pre-redirect URL when annotating downloaded subresources with Mark-of-the-Web (MOTW). An attacker can leverage an open redirect on an Intranet domain to serve a malicious archive, assigning it an Intranet MOTW (ZoneId=1). This can bypass Windows SmartScreen protections when the extracted payload is executed.

Affected files:

  • content/browser/download/save_file_manager.cc
  • content/browser/download/save_package.cc

Estimated timestamp from git blame: 2025-10-22

Summary

When a user saves a webpage using the “Webpage, Complete” feature, Chrome downloads the page’s subresources. If a subresource download involves an HTTP redirect, Chrome incorrectly annotates the downloaded file with Mark-of-the-Web (MOTW) information based on the initial pre-redirect URL rather than the final destination URL.

In enterprise environments where internal domains are mapped to the “Local Intranet” security zone (ZoneId=1), an attacker who can leverage an open redirect on a trusted domain can use this flaw to deliver malicious payloads that bypass Windows SmartScreen. By delivering the payload within a format considered NOT_DANGEROUS by Chrome’s Safe Browsing policies (e.g., a .zip archive), the attacker avoids Chrome’s defense-in-depth file renaming mitigations. When the user extracts the archive using Windows Explorer, the Intranet MOTW propagates to the extracted executable, allowing it to run without SmartScreen warnings.

Technical Details

  1. When SavePackage discovers a subresource, it initiates a save request via SaveFileManager::SaveURL (content/browser/download/save_file_manager.cc).
  2. SaveURL constructs a quarantine_callback to be executed when the download finishes. This callback is created using base::BindOnce and captures the original pre-redirect url by value (lines 235-239).
  3. The network request is made. If the server responds with a redirect, the SimpleURLLoader follows it to the final_url.
  4. When the response starts, SimpleURLLoaderHelper::OnResponseStarted is called. It parses the Content-Disposition header from the final response (e.g., attachment; filename="payload.zip") and creates a SaveFileCreateInfo object. It moves the existing quarantine_callback into this object without updating the bound URL.
  5. On the UI thread, SavePackage::GenerateFileName is called. It uses net::GenerateFileNameImpl, which prioritizes the filename from the Content-Disposition header. The file is named payload.zip.
  6. Because .zip is considered NOT_DANGEROUS in download_file_types.asciipb, ChromeDownloadManagerDelegate::SanitizeSavePackageResourceName does not append a .download extension to the file.
  7. Once the download completes, SaveFinished executes the bound quarantine_callback.
  8. The callback invokes SaveFileManager::QuarantineItem, which receives the original Intranet URL. This URL is eventually passed to Windows via quarantine::InvokeAttachmentServices.
  9. Windows writes the :Zone.Identifier stream to payload.zip. Because the original URL is in the Local Intranet zone, Windows sets ZoneId=1.
  10. When the user extracts the ZIP using Windows Explorer, the ZoneId=1 MOTW is propagated to the extracted executable. Double-clicking the executable bypasses SmartScreen Application Reputation checks because the file is considered to have originated from a trusted Intranet source.

Potential Exploitation Steps

Please note our tooling agent cannot execute code; these are suggested steps to verify the vulnerability.

  1. On a Windows machine, add a domain (e.g., internal.example.com) to the “Local Intranet” zone via Internet Options (Security tab).
  2. Prepare a malicious executable and compress it into a payload.zip file.
  3. Host the payload.zip file on an attacker-controlled server (https://attacker.evil/payload.zip), ensuring the server responds with a Content-Disposition: attachment; filename="payload.zip" header.
  4. Prepare a webpage that includes a subresource (e.g., an <img> tag) with a src pointing to a redirector on the trusted Intranet domain (e.g., https://internal.example.com/redirect?url=https://attacker.evil/payload.zip).
  5. Open the webpage in Chrome.
  6. Press Ctrl+S and save the page as “Webpage, Complete”.
  7. Navigate to the associated _files folder created by Chrome.
  8. Check the Zone Identifier for the downloaded payload.zip file using PowerShell: Get-Content -Path 'C:\path\to\saved_page_files\payload.zip' -Stream Zone.Identifier
  9. Observe that ZoneId=1 and HostUrl points to internal.example.com.
  10. Extract the ZIP file using Windows Explorer. Verify the extracted executable also has ZoneId=1.
  11. Run the extracted executable to confirm SmartScreen warnings are bypassed.

Suggested Fix

The quarantine_callback should not capture the URL by value before the network request begins. Instead, the callback signature should be updated to accept the final URL as a parameter when invoked at the end of the download. Alternatively, SimpleURLLoaderHelper::OnResponseStarted should re-bind the quarantine_callback with the final_url before passing it to SaveFileCreateInfo.

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


Results 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