Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in Downloads
DescriptionUI misrepresentation in Downloads
ComponentDownloads
Sub-componentDownloads
Bug ClassSpoofing
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.
Tracker514055890
Fix commit313c1ce85165 (chromium/src) +104/-23
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-17 (per vendor advisory)
Fix landed2026-08-05 (public git)
Disclosed2026-09-08
Patch gap34 days — fix public in source before the advisory

Background

`DownloadRequestLimiter::TabDownloadState`
the per-tab object that tracks a tab’s download permission state and holds the queue of pending download decisions while a prompt is displayed.
Multiple-download prompt
the permission UI Chrome shows when a page tries to start more than one automatic download in a tab, asking the user to allow or block further downloads for that site.
`callbacks_`
the per-tab list of pending download-decision callbacks that are waiting for the user’s answer to the prompt; each callback is later invoked with allow == true or false.
`NotifyCallbacks`
the method that drains callbacks_ and runs each queued callback with the user’s allow/deny decision once the prompt is resolved.

Root Cause Analysis

Before the patch, callbacks_ was a flat std::vector<DownloadRequestLimiter::Callback> that recorded no information about which origin initiated each queued download. While the multiple-download prompt was showing for one origin, any download initiated by a different origin sharing the same tab (for example a cross-origin subframe) was appended to that same queue via PromptUserForDownload, because is_showing_prompt() short-circuits and the request simply joins the pending list. When the user answered the prompt, Accept called NotifyCallbacks(true), which released every queued callback with allow == true regardless of origin, violating the invariant that a user’s permission decision applies only to the origin the prompt was actually shown for.

The fix changes callbacks_ to a std::vector<std::pair<url::Origin, DownloadRequestLimiter::Callback>>, tags each callback with its request_origin at enqueue time, and reworks NotifyCallbacks(request_origin, allow) to split the queue into callbacks matching request_origin (which receive allow) and all others (which are unconditionally cancelled with false). This restores per-origin scoping so accepting a prompt for one site can no longer authorize another site’s queued downloads.

Key insight
The single mistake was storing queued download callbacks without their originating origin, so a per-origin user decision was applied tab-wide; the fix pairs every callback with its request_origin and makes NotifyCallbacks grant allow only to callbacks whose origin matches the prompt’s origin while cancelling the rest.

Attack Path

  1. Establish prompting state for the attacker origin Attacker-controlled origin B (e.g. loaded as a subframe or via navigation in a tab) triggers enough downloads that TabDownloadState enters the prompt state and the multiple-download prompt is displayed to the user for origin B.
  2. Queue cross-origin downloads behind the visible prompt While the prompt is showing, another origin A active in the same tab calls CanDownload; because is_showing_prompt() is true, PromptUserForDownload appends A’s callback to the shared callbacks_ queue with no origin tagging in the vulnerable code.
  3. User resolves the prompt for the shown origin The user, seeing a prompt attributed to origin B, clicks allow, driving TabDownloadState::Accept and the pre-patch NotifyCallbacks(true).
  4. All queued callbacks released as allowed NotifyCallbacks(true) swaps out the entire callbacks_ vector and posts every callback with allow == true, so origin A’s queued downloads proceed even though the user only agreed to origin B’s prompt (and vice-versa), producing the UI misrepresentation / spoof.

Impact Assessment

An attacker gains the ability to have downloads from an origin the user never approved (or was never shown) proceed by piggy-backing on another origin’s accepted multiple-download prompt, all within the browser-process download-permission logic driven by web content in the tab. The result is a spoofing / UI-misrepresentation weakness: the download-permission dialog no longer faithfully represents which site’s downloads are being authorized. Preconditions are that two different origins are active in the same tab and that a multiple-download prompt is on screen for one of them when the other’s download decision is queued; no memory corruption or privilege escalation is demonstrated by the patch, and the advisory classifies this as a Downloads spoofing issue rather than a code-execution bug.

Changed Functions

FunctionChangeNotes
for
chrome/browser/download/download_request_limiter.cc
modified
if
chrome/browser/download/download_request_limiter.cc
modified
TEST_F
chrome/browser/download/download_request_limiter_unittest.cc
modified
for
chrome/browser/download/download_request_limiter_unittest.cc
modified
EXPECT_TRUE
chrome/browser/download/download_request_limiter_unittest.cc
modified

Files Changed

  • chrome/browser/download/download_request_limiter_unittest.cc
  • chrome/browser/download/download_request_limiter.cc
  • chrome/browser/download/download_request_limiter.h

Audit Directions

  • Shared per-tab/per-container queues keyed by nothing
    Audit any structure that batches pending security decisions (downloads, permissions, dialogs) across origins in one tab or WebContents and later resolves them in bulk; confirm each entry records and is filtered by its initiating origin before a single user decision is applied.
  • Prompt-origin vs. request-origin mismatch
    Look for permission flows where is_showing_prompt()-style short-circuits let a later, different-origin request attach to an in-flight prompt, so the resolution applies the prompt origin’s answer to a request from another origin.
  • Bulk `swap`/drain-all release patterns
    Flag code that swaps or clears an entire callback/task vector and applies one boolean outcome to all elements; verify the outcome is per-element scoped rather than uniformly applied across mixed-origin entries.
From 313c1ce85165833b914300836bee74222342b4d3 Mon Sep 17 00:00:00 2001
From: Yaw Frempong <yawfrempong@google.com>
Date: Wed, 05 Aug 2026 15:16:57 -0700
Subject: [PATCH] [Downloads] Scope queued download callbacks to requesting origin

DownloadRequestLimiter::TabDownloadState queues pending download
decisions in a single per-tab vector while the multiple-download
permission prompt is showing. Downloads initiated by an origin other
than the one shown in the prompt would join that queue and be released
with allow=true when the user accepted the prompt.

Tag each queued callback with its requesting origin and have
NotifyCallbacks() apply the user's decision only to callbacks that match
the prompt's origin; queued callbacks for any other origin are
cancelled.

Reviewed in https://crrev.com/i/9588737

Bug: 514055890
Change-Id: I858c3cb6fc07faacee96cd4c2cd073a003535f7d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8165050
Reviewed-by: Min Qin <qinmin@chromium.org>
Commit-Queue: Yaw Frempong <yawfrempong@google.com>
Cr-Commit-Position: refs/heads/main@{#1674546}
---
diff --git a/chrome/browser/download/download_request_limiter.cc b/chrome/browser/download/download_request_limiter.cc
index eb1ace9..011a277 100644
--- a/chrome/browser/download/download_request_limiter.cc
+++ b/chrome/browser/download/download_request_limiter.cc
@@ -139,7 +139,7 @@
   if (!shouldClearDownloadState(navigation_handle))
     return;
 
-  NotifyCallbacks(false);
+  NotifyCallbacks(origin_, false);
   host_->Remove(this, web_contents());
 }
 
@@ -173,7 +173,7 @@
     // just initially navigated to this page. See http://crbug.com/40299431.
     // However, explicitly leave the limiter in place if the navigation was
     // renderer-initiated and we are in a prompt state.
-    NotifyCallbacks(false);
+    NotifyCallbacks(origin_, false);
     host_->Remove(this, web_contents());
     return;
     // WARNING: We've been deleted.
@@ -198,7 +198,7 @@
   // Tab closed, no need to handle closing the dialog as it's owned by the
   // WebContents.
 
-  NotifyCallbacks(false);
+  NotifyCallbacks(origin_, false);
   host_->Remove(this, web_contents());
   // WARNING: We've been deleted.
 }
@@ -206,7 +206,7 @@
 void DownloadRequestLimiter::TabDownloadState::PromptUserForDownload(
     DownloadRequestLimiter::Callback callback,
     const url::Origin& request_origin) {
-  callbacks_.push_back(std::move(callback));
+  callbacks_.emplace_back(request_origin, std::move(callback));
   DCHECK(web_contents_);
   if (is_showing_prompt())
     return;
@@ -263,14 +263,14 @@
 void DownloadRequestLimiter::TabDownloadState::Cancel(
     const url::Origin& request_origin) {
   SetContentSetting(CONTENT_SETTING_BLOCK, request_origin);
-  bool throttled = NotifyCallbacks(false);
+  bool throttled = NotifyCallbacks(request_origin, false);
   SetDownloadStatusAndNotify(request_origin, throttled ? PROMPT_BEFORE_DOWNLOAD
                                                        : DOWNLOADS_NOT_ALLOWED);
 }
 
 void DownloadRequestLimiter::TabDownloadState::CancelOnce(
     const url::Origin& request_origin) {
-  bool throttled = NotifyCallbacks(false);
+  bool throttled = NotifyCallbacks(request_origin, false);
   SetDownloadStatusAndNotify(request_origin, throttled ? PROMPT_BEFORE_DOWNLOAD
                                                        : DOWNLOADS_NOT_ALLOWED);
 }
@@ -278,7 +278,7 @@
 void DownloadRequestLimiter::TabDownloadState::Accept(
     const url::Origin& request_origin) {
   SetContentSetting(CONTENT_SETTING_ALLOW, request_origin);
-  bool throttled = NotifyCallbacks(true);
+  bool throttled = NotifyCallbacks(request_origin, true);
   SetDownloadStatusAndNotify(
       request_origin, throttled ? PROMPT_BEFORE_DOWNLOAD : ALLOW_ALL_DOWNLOADS);
 }
@@ -379,34 +379,52 @@
                                  setting);
 }
 
-bool DownloadRequestLimiter::TabDownloadState::NotifyCallbacks(bool allow) {
-  std::vector<DownloadRequestLimiter::Callback> callbacks;
+bool DownloadRequestLimiter::TabDownloadState::NotifyCallbacks(
+    const url::Origin& request_origin,
+    bool allow) {
+  std::vector<DownloadRequestLimiter::Callback> requesting_origin_callbacks;
+  std::vector<DownloadRequestLimiter::Callback> other_origin_callbacks;
+  for (auto& [origin, callback] : callbacks_) {
+    if (origin == request_origin) {
+      requesting_origin_callbacks.push_back(std::move(callback));
+    } else {
+      other_origin_callbacks.push_back(std::move(callback));
+    }
+  }
+  callbacks_.clear();
+
   bool throttled = false;
 
   // Selectively send first few notifications only if number of downloads exceed
   // kMaxDownloadsAtOnce. In that case, we also retain the infobar instance and
   // don't close it. If allow is false, we send all the notifications to cancel
   // all remaining downloads and close the infobar.
-  if (!allow || (callbacks_.size() < kMaxDownloadsAtOnce)) {
+  if (!allow || (requesting_origin_callbacks.size() < kMaxDownloadsAtOnce)) {
     // Null the generated weak pointer so we don't get notified again.
     factory_.InvalidateWeakPtrs();
-    callbacks.swap(callbacks_);
   } else {
-    std::vector<DownloadRequestLimiter::Callback>::iterator start, end;
-    start = callbacks_.begin();
-    end = callbacks_.begin() + kMaxDownloadsAtOnce;
-    callbacks.assign(std::make_move_iterator(start),
-                     std::make_move_iterator(end));
-    callbacks_.erase(start, end);
+    auto start = requesting_origin_callbacks.begin() + kMaxDownloadsAtOnce;
+    auto end = requesting_origin_callbacks.end();
+    for (auto it = start; it != end; ++it) {
+      callbacks_.emplace_back(request_origin, std::move(*it));
+    }
+    requesting_origin_callbacks.erase(start, end);
     throttled = true;
   }
 
-  for (auto& callback : callbacks) {
+  for (auto& callback : requesting_origin_callbacks) {
     // When callback runs, it can cause the WebContents to be destroyed.
     content::GetUIThreadTaskRunner({})->PostTask(
         FROM_HERE, base::BindOnce(std::move(callback), allow));
   }
 
+  // Downloads queued for other origins are not covered by the user's decision
+  // for `request_origin`, so cancel them.
+  for (auto& callback : other_origin_callbacks) {
+    content::GetUIThreadTaskRunner({})->PostTask(
+        FROM_HERE, base::BindOnce(std::move(callback), false));
+  }
+
   return throttled;
 }
 
diff --git a/chrome/browser/download/download_request_limiter.h b/chrome/browser/download/download_request_limiter.h
index e5b2770..d9bf211 100644
--- a/chrome/browser/download/download_request_limiter.h
+++ b/chrome/browser/download/download_request_limiter.h
@@ -10,6 +10,7 @@
 #include <map>
 #include <set>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "base/functional/callback.h"
@@ -176,8 +177,11 @@
                            const url::Origin& request_origin);
 
     // Notifies the callbacks as to whether the download is allowed or not.
-    // Returns false if it didn't notify all callbacks.
-    bool NotifyCallbacks(bool allow);
+    // Only callbacks queued for `request_origin` are eligible to be allowed;
+    // callbacks queued for any other origin are always notified with false.
+    // Returns true if downloads were throttled and remaining callbacks were
+    // kept queued.
+    bool NotifyCallbacks(const url::Origin& request_origin, bool allow);
 
     // Set the download limiter state and notify if it has changed. Callers must
     // guarantee that |status| and |setting| correspond to each other.
@@ -208,10 +212,11 @@
     // True if a download has been seen on the current page load.
     bool download_seen_;
 
-    // Callbacks we need to notify. This is only non-empty if we're showing a
-    // dialog.
+    // Callbacks we need to notify, paired with the origin that initiated each
+    // download. This is only non-empty if we're showing a dialog.
     // See description above CanDownload for details on lifetime of callbacks.
-    std::vector<DownloadRequestLimiter::Callback> callbacks_;
+    std::vector<std::pair<url::Origin, DownloadRequestLimiter::Callback>>
+        callbacks_;
 
     // Origins that have non-default download state.
     using DownloadStatusMap = std::map<url::Origin, DownloadStatus>;
diff --git a/chrome/browser/download/download_request_limiter_unittest.cc b/chrome/browser/download/download_request_limiter_unittest.cc
index d92148d7..259ceddc 100644
--- a/chrome/browser/download/download_request_limiter_unittest.cc
+++ b/chrome/browser/download/download_request_limiter_unittest.cc
@@ -9,6 +9,7 @@
 #include "base/command_line.h"
 #include "base/functional/bind.h"
 #include "base/run_loop.h"
+#include "base/test/run_until.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/time/time.h"
 #include "build/build_config.h"
@@ -138,6 +139,11 @@
     SetHostContentSetting(web_contents, setting);
   }
 
+  DownloadRequestLimiter::TabDownloadState* GetTabDownloadState(
+      WebContents* web_contents) {
+    return download_request_limiter_->GetDownloadState(web_contents);
+  }
+
  protected:
   const GURL kTestURL = GURL("http://foo.com/bar");
 
@@ -1025,6 +1031,58 @@
             download_request_limiter_->GetDownloadUiStatus(web_contents()));
 }
 
+// Test that accepting the multiple-download prompt only allows the queued
+// downloads that were initiated by the origin the prompt was shown for.
+TEST_F(DownloadRequestLimiterTest, AcceptOnlyAllowsPromptOriginDownloads) {
+  NavigateAndCommit(kTestURL);
+  LoadCompleted();
+
+  url::Origin main_origin = url::Origin::Create(kTestURL);
+  url::Origin other_origin = url::Origin::Create(GURL("http://foobar.com"));
+
+  // First download from the main origin is allowed and moves the tab to
+  // PROMPT_BEFORE_DOWNLOAD.
+  CanDownloadFor(kTestURL, web_contents(), main_origin);
+  ExpectAndResetCounts(1, 0, 0, __LINE__);
+  EXPECT_EQ(DownloadRequestLimiter::PROMPT_BEFORE_DOWNLOAD,
+            download_request_limiter_->GetDownloadStatus(web_contents()));
+
+  // A download from another origin in the same tab triggers the prompt for that
+  // origin. Leave the prompt visible so subsequent downloads queue behind it.
+  UpdateExpectations(WAIT);
+  CanDownloadFor(kTestURL, web_contents(), other_origin);
+  EXPECT_TRUE(mock_permission_prompt_factory_->RequestOriginSeen(
+      other_origin.GetURL()));
+  EXPECT_FALSE(
+      mock_permission_prompt_factory_->RequestOriginSeen(main_origin.GetURL()));
+  ExpectAndResetCounts(0, 0, 1, __LINE__);
+
+  // While the prompt is showing, queue several more downloads from the main
+  // origin. These must not be released by accepting the other origin's prompt.
+  for (int i = 0; i < 5; ++i) {
+    CanDownloadFor(kTestURL, web_contents(), main_origin);
+  }
+  ExpectAndResetCounts(0, 0, 0, __LINE__);
+
+  // Accept the prompt as the permission request would for `other_origin`.
+  DownloadRequestLimiter::TabDownloadState* state =
+      GetTabDownloadState(web_contents());
+  ASSERT_TRUE(state);
+  state->Accept(other_origin);
+  EXPECT_TRUE(base::test::RunUntil([&]() {
+    return state->GetDownloadStatus(other_origin) ==
+           DownloadRequestLimiter::ALLOW_ALL_DOWNLOADS;
+  }));
+
+  // Only the download queued for `other_origin` should proceed; downloads
+  // queued for the main origin should be cancelled.
+  ExpectAndResetCounts(1, 5, 0, __LINE__);
+  EXPECT_EQ(DownloadRequestLimiter::ALLOW_ALL_DOWNLOADS,
+            state->GetDownloadStatus(other_origin));
+  EXPECT_EQ(DownloadRequestLimiter::PROMPT_BEFORE_DOWNLOAD,
+            state->GetDownloadStatus(main_origin));
+}
+
 // Test that user interaction on the current page won't reset download status
 // for another origin.
 TEST_F(DownloadRequestLimiterTest,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/download/download_request_limiter_unittest.cc b/chrome/browser/download/download_request_limiter_unittest.cc
index d92148d7..259ceddc 100644
--- a/chrome/browser/download/download_request_limiter_unittest.cc
+++ b/chrome/browser/download/download_request_limiter_unittest.cc
@@ -9,6 +9,7 @@
 #include "base/command_line.h"
 #include "base/functional/bind.h"
 #include "base/run_loop.h"
+#include "base/test/run_until.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/time/time.h"
 #include "build/build_config.h"
@@ -138,6 +139,11 @@
     SetHostContentSetting(web_contents, setting);
   }
 
+  DownloadRequestLimiter::TabDownloadState* GetTabDownloadState(
+      WebContents* web_contents) {
+    return download_request_limiter_->GetDownloadState(web_contents);
+  }
+
  protected:
   const GURL kTestURL = GURL("http://foo.com/bar");
 
@@ -1025,6 +1031,58 @@
             download_request_limiter_->GetDownloadUiStatus(web_contents()));
 }
 
+// Test that accepting the multiple-download prompt only allows the queued
+// downloads that were initiated by the origin the prompt was shown for.
+TEST_F(DownloadRequestLimiterTest, AcceptOnlyAllowsPromptOriginDownloads) {
+  NavigateAndCommit(kTestURL);
+  LoadCompleted();
+
+  url::Origin main_origin = url::Origin::Create(kTestURL);
+  url::Origin other_origin = url::Origin::Create(GURL("http://foobar.com"));
+
+  // First download from the main origin is allowed and moves the tab to
+  // PROMPT_BEFORE_DOWNLOAD.
+  CanDownloadFor(kTestURL, web_contents(), main_origin);
+  ExpectAndResetCounts(1, 0, 0, __LINE__);
+  EXPECT_EQ(DownloadRequestLimiter::PROMPT_BEFORE_DOWNLOAD,
+            download_request_limiter_->GetDownloadStatus(web_contents()));
+
+  // A download from another origin in the same tab triggers the prompt for that
+  // origin. Leave the prompt visible so subsequent downloads queue behind it.
+  UpdateExpectations(WAIT);
+  CanDownloadFor(kTestURL, web_contents(), other_origin);
+  EXPECT_TRUE(mock_permission_prompt_factory_->RequestOriginSeen(
+      other_origin.GetURL()));
+  EXPECT_FALSE(
+      mock_permission_prompt_factory_->RequestOriginSeen(main_origin.GetURL()));
+  ExpectAndResetCounts(0, 0, 1, __LINE__);
+
+  // While the prompt is showing, queue several more downloads from the main
+  // origin. These must not be released by accepting the other origin's prompt.
+  for (int i = 0; i < 5; ++i) {
+    CanDownloadFor(kTestURL, web_contents(), main_origin);
+  }
+  ExpectAndResetCounts(0, 0, 0, __LINE__);
+
+  // Accept the prompt as the permission request would for `other_origin`.
+  DownloadRequestLimiter::TabDownloadState* state =
+      GetTabDownloadState(web_contents());
+  ASSERT_TRUE(state);
+  state->Accept(other_origin);
+  EXPECT_TRUE(base::test::RunUntil([&]() {
+    return state->GetDownloadStatus(other_origin) ==
+           DownloadRequestLimiter::ALLOW_ALL_DOWNLOADS;
+  }));
+
+  // Only the download queued for `other_origin` should proceed; downloads
+  // queued for the main origin should be cancelled.
+  ExpectAndResetCounts(1, 5, 0, __LINE__);
+  EXPECT_EQ(DownloadRequestLimiter::ALLOW_ALL_DOWNLOADS,
+            state->GetDownloadStatus(other_origin));
+  EXPECT_EQ(DownloadRequestLimiter::PROMPT_BEFORE_DOWNLOAD,
+            state->GetDownloadStatus(main_origin));
+}
+
 // Test that user interaction on the current page won't reset download status
 // for another origin.
 TEST_F(DownloadRequestLimiterTest,
Loading diff…

Original Bug Report

reported by vm...@google.com

Cross-origin download permission bypass via shared TabDownloadState callback queue

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 logic vulnerability in DownloadRequestLimiter allows an attacker to bypass download protections by piggybacking malicious requests onto a permission prompt labeled with a different origin. This is caused by an origin-agnostic callback queue shared across all origins within a single tab.

Affected files:

  • chrome/browser/download/download_request_limiter.cc
  • chrome/browser/download/download_request_limiter.h

Estimated timestamp from git blame: 2019-06-25

Summary

A potential logic flaw in DownloadRequestLimiter::TabDownloadState allows an attacker to bypass multi-download (carpet-bombing) protections. By carefully timing download requests, an attacker-controlled origin can cause the browser to release a queue of unauthorized downloads using a permission prompt that the user believes is attributed to a different, potentially trusted, origin.

Technical Details

The DownloadRequestLimiter::TabDownloadState class (defined in chrome/browser/download/download_request_limiter.h) manages the download throttling state for a single WebContents (tab). It maintains a shared queue of pending download decision callbacks: std::vector<Callback> callbacks_ (line 210).

When PromptUserForDownload is called in chrome/browser/download/download_request_limiter.cc:

  1. The callback is appended to the shared callbacks_ vector (line 209).
  2. If a permission prompt is already being displayed (is_showing_prompt() returns true), the function returns early without creating a new prompt (lines 211-212).
  3. If no prompt is visible, a DownloadPermissionRequest is initiated using the request_origin of the caller. This origin is displayed to the user in the permission bubble.

The flaw occurs during the resolution of the prompt. When a user clicks ‘Allow’, TabDownloadState::Accept is invoked, which calls NotifyCallbacks(true) (line 282). The NotifyCallbacks function (lines 383-412) drains the shared callbacks_ vector and executes all queued callbacks with allow=true.

Critically, the implementation does not verify that the origin associated with each individual queued callback matches the origin for which the user actually granted permission. Consequently, any requests queued while a prompt for a different origin is active will be silently allowed upon the user’s acceptance of that prompt.

Potential Attack Scenario

  1. An attacker at https://evil.example initiates a single download to consume the initial ALLOW_ONE_DOWNLOAD quota for the tab.
  2. The attacker triggers a download from a different origin (e.g., via a hidden iframe to https://trusted.example), causing the browser to display a permission prompt labeled ’trusted.example wants to download multiple files’.
  3. While this prompt is visible, evil.example triggers numerous additional download requests via script. These are silently added to the shared callbacks_ queue because a prompt is already showing.
  4. The user, trusting trusted.example, clicks ‘Allow’.
  5. NotifyCallbacks executes the entire queue, releasing all pending downloads from evil.example simultaneously.

Suggested Fix

Modify TabDownloadState to track the origin associated with each pending callback. The callbacks_ queue should be replaced with a structure (such as a map) that separates callbacks by origin. NotifyCallbacks should be updated to only execute callbacks that match the specific origin for which the user interacted with the prompt.

Note: These steps and the impact are based on code analysis and represent a potential vulnerability; a functional proof-of-concept has not been executed.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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