Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in FileAPI
DescriptionImproper input validation in FileAPI
ComponentFileAPI
Bug ClassLogic Error
Tracker532957878
Fix commit6617d58a8cc7 (chromium/src) +185/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
FakeDelegate
components/download/internal/common/download_response_handler_unittest.cc
modified
DownloadResponseHandlerTest
components/download/internal/common/download_response_handler_unittest.cc
modified
TEST_F
components/download/internal/common/download_response_handler_unittest.cc
modified

Files Changed

  • components/download/internal/common/BUILD.gn
  • components/download/internal/common/DEPS
  • components/download/internal/common/download_response_handler.cc
  • components/download/internal/common/download_response_handler_unittest.cc
From 6617d58a8cc7734783373152143f70a974afc9a0 Mon Sep 17 00:00:00 2001
From: Minoru Chikamune <chikamune@chromium.org>
Date: Mon, 03 Aug 2026 03:12:51 -0700
Subject: [PATCH] [download] Reject redirects for non-HTTP download request URLs

DownloadResponseHandler::OnReceiveRedirect() unconditionally appended
the redirect target to the download's URL chain. Only HTTP(S) responses
can produce HTTP redirects; loaders for local schemes such as blob: and
data: never issue them, so treat any redirect reported while the current
request URL is not HTTP(S) as an invalid request instead of following
it. This keeps the resulting DownloadItem's URL chain and referrer bound
to the URL that was actually loaded.

Add DownloadResponseHandlerTest to cover the blob:/data: cases in both
kFollow and kManual redirect modes and to anchor that HTTP(S) redirects
are still followed.

TAG=agy

Fixed: 532957878
Change-Id: I6441a09bba8cd4d0d9db0ac95bc6127f4d5d13c7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8185129
Reviewed-by: Shakti Sahu <shaktisahu@chromium.org>
Commit-Queue: Minoru Chikamune <chikamune@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1672529}
---

diff --git a/components/download/internal/common/BUILD.gn b/components/download/internal/common/BUILD.gn
index 92e1c31..3d8b3fc 100644
--- a/components/download/internal/common/BUILD.gn
+++ b/components/download/internal/common/BUILD.gn
@@ -174,6 +174,7 @@
     "download_item_impl_unittest.cc",
     "download_job_factory_unittest.cc",
     "download_path_reservation_tracker_unittest.cc",
+    "download_response_handler_unittest.cc",
     "download_stats_unittest.cc",
     "download_ukm_helper_unittest.cc",
     "download_utils_unittest.cc",
diff --git a/components/download/internal/common/DEPS b/components/download/internal/common/DEPS
index 5ecffaa..5517c38 100644
--- a/components/download/internal/common/DEPS
+++ b/components/download/internal/common/DEPS
@@ -36,3 +36,10 @@
   "+services/network/public/mojom",
   "+services/service_manager/public/cpp",
 ]
+
+specific_include_rules = {
+  ".*_unittest\\.cc": [
+    "+net/url_request/redirect_info.h",
+  ],
+}
+
diff --git a/components/download/internal/common/download_response_handler.cc b/components/download/internal/common/download_response_handler.cc
index 5c6aeac..1f87d05 100644
--- a/components/download/internal/common/download_response_handler.cc
+++ b/components/download/internal/common/download_response_handler.cc
@@ -212,6 +212,16 @@
 void DownloadResponseHandler::OnReceiveRedirect(
     const net::RedirectInfo& redirect_info,
     network::mojom::URLResponseHeadPtr head) {
+  // Only responses to HTTP(S) requests can produce redirects. Loaders for
+  // local schemes such as blob: and data: never issue them, so treat a
+  // redirect while the current request URL is not HTTP(S) as invalid rather
+  // than appending the target URL to the download's URL chain.
+  if (url_chain_.empty() || !url_chain_.back().SchemeIsHTTPOrHTTPS()) {
+    abort_reason_ = DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST;
+    OnComplete(network::URLLoaderCompletionStatus(net::OK));
+    return;
+  }
+
   // Check if redirect URL is web safe.
   if (delegate_ && !delegate_->CanRequestURL(redirect_info.new_url)) {
     abort_reason_ = DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST;
diff --git a/components/download/internal/common/download_response_handler_unittest.cc b/components/download/internal/common/download_response_handler_unittest.cc
new file mode 100644
index 0000000..fc52b38
--- /dev/null
+++ b/components/download/internal/common/download_response_handler_unittest.cc
@@ -0,0 +1,167 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/download/public/common/download_response_handler.h"
+
+#include <memory>
+#include <utility>
+
+#include "components/download/public/common/download_create_info.h"
+#include "components/download/public/common/download_interrupt_reasons.h"
+#include "components/download/public/common/download_save_info.h"
+#include "components/download/public/common/download_url_parameters.h"
+#include "net/url_request/redirect_info.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/fetch_api.mojom-shared.h"
+#include "services/network/public/mojom/url_response_head.mojom.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace download {
+namespace {
+
+class FakeDelegate : public DownloadResponseHandler::Delegate {
+ public:
+  FakeDelegate() = default;
+  ~FakeDelegate() = default;
+
+  // DownloadResponseHandler::Delegate:
+  void OnResponseStarted(
+      std::unique_ptr<DownloadCreateInfo> download_create_info,
+      mojom::DownloadStreamHandlePtr stream_handle) override {
+    create_info_ = std::move(download_create_info);
+  }
+  void OnReceiveRedirect() override { ++redirect_count_; }
+  void OnResponseCompleted() override { response_completed_ = true; }
+  bool CanRequestURL(const GURL& url) override { return true; }
+  void OnUploadProgress(uint64_t bytes_uploaded) override {}
+
+  const DownloadCreateInfo* create_info() const { return create_info_.get(); }
+  int redirect_count() const { return redirect_count_; }
+  bool response_completed() const { return response_completed_; }
+
+ private:
+  std::unique_ptr<DownloadCreateInfo> create_info_;
+  int redirect_count_ = 0;
+  bool response_completed_ = false;
+};
+
+class DownloadResponseHandlerTest : public testing::Test {
+ protected:
+  std::unique_ptr<DownloadResponseHandler> CreateHandler(
+      const GURL& request_url,
+      network::mojom::RedirectMode cross_origin_redirects) {
+    resource_request_.url = request_url;
+    resource_request_.method = "GET";
+    return std::make_unique<DownloadResponseHandler>(
+        &resource_request_, &delegate_, std::make_unique<DownloadSaveInfo>(),
+        /*is_parallel_request=*/false,
+        /*is_transient=*/false,
+        /*fetch_error_body=*/false, cross_origin_redirects,
+        DownloadUrlParameters::RequestHeadersType(),
+        /*request_origin=*/std::string(), DownloadSource::UNKNOWN,
+        /*require_safety_checks=*/true, std::vector<GURL>(1, request_url),
+        /*is_background_mode=*/false);
+  }
+
+  static net::RedirectInfo MakeRedirectInfo(const GURL& new_url) {
+    net::RedirectInfo redirect_info;
+    redirect_info.status_code = 302;
+    redirect_info.new_method = "GET";
+    redirect_info.new_url = new_url;
+    return redirect_info;
+  }
+
+  network::ResourceRequest resource_request_;
+  FakeDelegate delegate_;
+};
+
+TEST_F(DownloadResponseHandlerTest, HttpRequestFollowsRedirect) {
+  const GURL kRequestUrl("https://a.example.com/file");
+  const GURL kRedirectUrl("https://b.example.com/file");
+  auto handler =
+      CreateHandler(kRequestUrl, network::mojom::RedirectMode::kFollow);
+
+  handler->OnReceiveRedirect(MakeRedirectInfo(kRedirectUrl),
+                             network::mojom::URLResponseHead::New());
+
+  EXPECT_EQ(1, delegate_.redirect_count());
+  EXPECT_FALSE(delegate_.response_completed());
+  EXPECT_FALSE(delegate_.create_info());
+}
+
+// Loading a blob: URL never produces an HTTP redirect, so a redirect reported
+// while the current request URL is blob: must not be followed and must not be
+// appended to the download's URL chain.
+TEST_F(DownloadResponseHandlerTest, BlobRequestRejectsRedirect) {
+  const GURL kBlobUrl("blob:https://a.example.com/1234");
+  const GURL kRedirectUrl("https://b.example.com/payload");
+  auto handler = CreateHandler(kBlobUrl, network::mojom::RedirectMode::kFollow);
+
+  handler->OnReceiveRedirect(MakeRedirectInfo(kRedirectUrl),
+                             network::mojom::URLResponseHead::New());
+
+  EXPECT_EQ(0, delegate_.redirect_count());
+  EXPECT_TRUE(delegate_.response_completed());
+  ASSERT_TRUE(delegate_.create_info());
+  EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST,
+            delegate_.create_info()->result);
+  ASSERT_EQ(1u, delegate_.create_info()->url_chain.size());
+  EXPECT_EQ(kBlobUrl, delegate_.create_info()->url_chain.back());
+}
+
+// Same as above with the manual redirect mode: the redirect target must not be
+// surfaced to the delegate as an interrupted cross-origin redirect either.
+TEST_F(DownloadResponseHandlerTest, BlobRequestRejectsManualRedirect) {
+  const GURL kBlobUrl("blob:https://a.example.com/1234");
+  const GURL kRedirectUrl("https://b.example.com/payload");
+  auto handler = CreateHandler(kBlobUrl, network::mojom::RedirectMode::kManual);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/download/internal/common/download_response_handler_unittest.cc b/components/download/internal/common/download_response_handler_unittest.cc
new file mode 100644
index 0000000..fc52b38
--- /dev/null
+++ b/components/download/internal/common/download_response_handler_unittest.cc
@@ -0,0 +1,167 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/download/public/common/download_response_handler.h"
+
+#include <memory>
+#include <utility>
+
+#include "components/download/public/common/download_create_info.h"
+#include "components/download/public/common/download_interrupt_reasons.h"
+#include "components/download/public/common/download_save_info.h"
+#include "components/download/public/common/download_url_parameters.h"
+#include "net/url_request/redirect_info.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/fetch_api.mojom-shared.h"
+#include "services/network/public/mojom/url_response_head.mojom.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace download {
+namespace {
+
+class FakeDelegate : public DownloadResponseHandler::Delegate {
+ public:
+  FakeDelegate() = default;
+  ~FakeDelegate() = default;
+
+  // DownloadResponseHandler::Delegate:
+  void OnResponseStarted(
+      std::unique_ptr<DownloadCreateInfo> download_create_info,
+      mojom::DownloadStreamHandlePtr stream_handle) override {
+    create_info_ = std::move(download_create_info);
+  }
+  void OnReceiveRedirect() override { ++redirect_count_; }
+  void OnResponseCompleted() override { response_completed_ = true; }
+  bool CanRequestURL(const GURL& url) override { return true; }
+  void OnUploadProgress(uint64_t bytes_uploaded) override {}
+
+  const DownloadCreateInfo* create_info() const { return create_info_.get(); }
+  int redirect_count() const { return redirect_count_; }
+  bool response_completed() const { return response_completed_; }
+
+ private:
+  std::unique_ptr<DownloadCreateInfo> create_info_;
+  int redirect_count_ = 0;
+  bool response_completed_ = false;
+};
+
+class DownloadResponseHandlerTest : public testing::Test {
+ protected:
+  std::unique_ptr<DownloadResponseHandler> CreateHandler(
+      const GURL& request_url,
+      network::mojom::RedirectMode cross_origin_redirects) {
+    resource_request_.url = request_url;
+    resource_request_.method = "GET";
+    return std::make_unique<DownloadResponseHandler>(
+        &resource_request_, &delegate_, std::make_unique<DownloadSaveInfo>(),
+        /*is_parallel_request=*/false,
+        /*is_transient=*/false,
+        /*fetch_error_body=*/false, cross_origin_redirects,
+        DownloadUrlParameters::RequestHeadersType(),
+        /*request_origin=*/std::string(), DownloadSource::UNKNOWN,
+        /*require_safety_checks=*/true, std::vector<GURL>(1, request_url),
+        /*is_background_mode=*/false);
+  }
+
+  static net::RedirectInfo MakeRedirectInfo(const GURL& new_url) {
+    net::RedirectInfo redirect_info;
+    redirect_info.status_code = 302;
+    redirect_info.new_method = "GET";
+    redirect_info.new_url = new_url;
+    return redirect_info;
+  }
+
+  network::ResourceRequest resource_request_;
+  FakeDelegate delegate_;
+};
+
+TEST_F(DownloadResponseHandlerTest, HttpRequestFollowsRedirect) {
+  const GURL kRequestUrl("https://a.example.com/file");
+  const GURL kRedirectUrl("https://b.example.com/file");
+  auto handler =
+      CreateHandler(kRequestUrl, network::mojom::RedirectMode::kFollow);
+
+  handler->OnReceiveRedirect(MakeRedirectInfo(kRedirectUrl),
+                             network::mojom::URLResponseHead::New());
+
+  EXPECT_EQ(1, delegate_.redirect_count());
+  EXPECT_FALSE(delegate_.response_completed());
+  EXPECT_FALSE(delegate_.create_info());
+}
+
+// Loading a blob: URL never produces an HTTP redirect, so a redirect reported
+// while the current request URL is blob: must not be followed and must not be
+// appended to the download's URL chain.
+TEST_F(DownloadResponseHandlerTest, BlobRequestRejectsRedirect) {
+  const GURL kBlobUrl("blob:https://a.example.com/1234");
+  const GURL kRedirectUrl("https://b.example.com/payload");
+  auto handler = CreateHandler(kBlobUrl, network::mojom::RedirectMode::kFollow);
+
+  handler->OnReceiveRedirect(MakeRedirectInfo(kRedirectUrl),
+                             network::mojom::URLResponseHead::New());
+
+  EXPECT_EQ(0, delegate_.redirect_count());
+  EXPECT_TRUE(delegate_.response_completed());
+  ASSERT_TRUE(delegate_.create_info());
+  EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST,
+            delegate_.create_info()->result);
+  ASSERT_EQ(1u, delegate_.create_info()->url_chain.size());
+  EXPECT_EQ(kBlobUrl, delegate_.create_info()->url_chain.back());
+}
+
+// Same as above with the manual redirect mode: the redirect target must not be
+// surfaced to the delegate as an interrupted cross-origin redirect either.
+TEST_F(DownloadResponseHandlerTest, BlobRequestRejectsManualRedirect) {
+  const GURL kBlobUrl("blob:https://a.example.com/1234");
+  const GURL kRedirectUrl("https://b.example.com/payload");
+  auto handler = CreateHandler(kBlobUrl, network::mojom::RedirectMode::kManual);
+
+  handler->OnReceiveRedirect(MakeRedirectInfo(kRedirectUrl),
+                             network::mojom::URLResponseHead::New());
+
+  EXPECT_EQ(0, delegate_.redirect_count());
+  ASSERT_TRUE(delegate_.create_info());
+  EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST,
+            delegate_.create_info()->result);
+  ASSERT_EQ(1u, delegate_.create_info()->url_chain.size());
+  EXPECT_EQ(kBlobUrl, delegate_.create_info()->url_chain.back());
+}
+
+TEST_F(DownloadResponseHandlerTest, DataRequestRejectsRedirect) {
+  const GURL kDataUrl("data:text/plain,hello");
+  const GURL kRedirectUrl("https://b.example.com/payload");
+  auto handler = CreateHandler(kDataUrl, network::mojom::RedirectMode::kFollow);
+
+  handler->OnReceiveRedirect(MakeRedirectInfo(kRedirectUrl),
+                             network::mojom::URLResponseHead::New());
+
+  EXPECT_EQ(0, delegate_.redirect_count());
+  EXPECT_TRUE(delegate_.response_completed());
+  ASSERT_TRUE(delegate_.create_info());
+  EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST,
+            delegate_.create_info()->result);
+  ASSERT_EQ(1u, delegate_.create_info()->url_chain.size());
+  EXPECT_EQ(kDataUrl, delegate_.create_info()->url_chain.back());
+}
+
+TEST_F(DownloadResponseHandlerTest, FileRequestRejectsRedirect) {
+  const GURL kFileUrl("file:///path/to/test.file");
+  const GURL kRedirectUrl("https://b.example.com/payload");
+  auto handler = CreateHandler(kFileUrl, network::mojom::RedirectMode::kFollow);
+
+  handler->OnReceiveRedirect(MakeRedirectInfo(kRedirectUrl),
+                             network::mojom::URLResponseHead::New());
+
+  EXPECT_EQ(0, delegate_.redirect_count());
+  EXPECT_TRUE(delegate_.response_completed());
+  ASSERT_TRUE(delegate_.create_info());
+  EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST,
+            delegate_.create_info()->result);
+  ASSERT_EQ(1u, delegate_.create_info()->url_chain.size());
+  EXPECT_EQ(kFileUrl, delegate_.create_info()->url_chain.back());
+}
+
+}  // namespace
+}  // namespace download
Loading diff…

Original Bug Report

reported by vm...@google.com

Forge of download source origin via renderer-hosted Blob and URLLoader redirection

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A compromised renderer can register a self-hosted blink::mojom::Blob and initiate a download of its blob URL. Because the browser forwards the URLLoaderClient remote directly to the renderer-hosted Blob, the renderer can fabricate redirect and response calls to forge the download’s canonical source URL and origin. This allows a compromised renderer to potentially spoof security indicators including Mark-of-the-Web, Safe Browsing checks, the download UI, and enterprise policies.

Affected files:

  • storage/browser/blob/blob_url_loader_factory.cc
  • storage/browser/blob/blob_url_store_impl.cc
  • components/download/internal/common/resource_downloader.cc
  • components/download/internal/common/download_response_handler.cc

Estimated timestamp from git blame: 2018-02-01

Summary / Potential Impact

There is a potential vulnerability in the download subsystem of Chromium where a compromised renderer can spoof the canonical source URL and origin of a downloaded file. By hosting its own implementation of blink::mojom::Blob and initiating a download, the renderer receives the browser’s URLLoaderClient Mojo remote (implemented by DownloadResponseHandler). This allows the compromised renderer to simulate a cross-origin redirect to any arbitrary HTTPS URL (e.g., https://victim-bank.com/secure/payload.exe).

As a result, the browser completes the download, writing the renderer-supplied bytes to disk while treating the file’s origin as the spoofed URL. This allows the renderer to potentially:

  • Spoof the source metadata on disk (Windows Zone.Identifier / Mark-of-the-Web, macOS quarantine data URL).
  • Bypass Safe Browsing download protection whitelists and reputation checks.
  • Spoof the origin displayed in the Chrome download bubble or shelf.
  • Trigger enterprise auto-open policies (e.g., AutoOpenAllowedForURLs) if the spoofed URL matches an allowlist.

Note: These steps are based on static code analysis and are potential/suggested steps; our tooling does not currently have the ability to run proof-of-concept exploit code.

Potential Step-by-Step Scenario

  1. Register a Renderer-Hosted Blob: A compromised renderer creates a Mojo pipe for blink.mojom.Blob, keeping the Receiver end and implementing a malicious, self-hosted Blob interface. It calls BlobURLStore::Register with a URL of its own origin (e.g., blob:https://evil.com/UUID) and the PendingRemote<Blob> endpoint.

  2. Obtain a Blob URL Token: The renderer resolves the registered blob URL as a token by calling BlobURLStore::ResolveAsBlobURLToken to receive a blink::mojom::BlobURLToken remote mapping to the renderer’s hosted Blob.

  3. Initiate the Download: The renderer calls LocalFrameHost::DownloadURL via Mojo, supplying the blob URL, the token, and setting cross_origin_redirects to network::mojom::RedirectMode::kFollow.

  4. Browser Plumbs the Loader Factory: The browser-process RenderFrameHostImpl::DownloadURL builds a BlobURLLoaderFactory wrapping the token’s remote, passes it to the InProgressDownloadManager, and executes the load on the IO thread via ResourceDownloader::Start.

  5. Handover of Mojo Endpoints: ResourceDownloader::Start creates a browser-side DownloadResponseHandler to act as the URLLoaderClient. It then calls CreateLoaderAndStart on the BlobURLLoaderFactory with a PendingReceiver<URLLoader> and the PendingRemote<URLLoaderClient>. Because the factory is backed by the renderer’s custom Blob, both endpoints are delivered straight to the renderer’s Blob::Load implementation.

  6. Fabricate a Redirection: The renderer intercepts the URLLoaderClient remote and calls OnReceiveRedirect with a spoofed RedirectInfo containing a target URL (e.g., https://victim-bank.com/secure/payload.exe) and referrer.

  7. Bypass Cross-Origin Restrictions: The browser’s DownloadResponseHandler::OnReceiveRedirect processes the redirect. Since cross_origin_redirects was set to kFollow by the renderer, the cross-origin check is bypassed, and the forged URL is appended to the download’s url_chain_.

  8. Serve the Payload: The renderer calls OnReceiveResponse on the URLLoaderClient remote, providing a fake URLResponseHead and its malicious payload via a Mojo data pipe. The download completes, and the spoofed URL (victim-bank.com) is recorded as the primary canonical source of the download.

Code References

  • Endpoint Hand-off: storage/browser/blob/blob_url_loader_factory.cc:80-81
  • Redirect Hand-off & URL Chain Manipulation: components/download/internal/common/download_response_handler.cc:212-257
  • Downstream Sinks (MotW & Quarantine): components/download/internal/common/download_item_impl.cc:2027-2036 and components/download/internal/common/base_file.cc:619-644

Suggested Fix

To prevent this vulnerability, the browser process should ensure that the blink::mojom::Blob remote used to fetch download resources is always browser-hosted.

Specifically, VerifyDownloadUrlParams or the DownloadURL entry point should validate that any blob_url_token supplied by the renderer resolves to a blob backed by storage::BlobImpl (or similar browser-controlled storage), rather than allowing arbitrary renderer-hosted implementations to handle the Load request.

Evaluated with Chrome root at commit: 84065d9121f6e48f67755f0ae963cc09617e5c85


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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