Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactClickjacking in Downloads
DescriptionClickjacking in Downloads
ComponentDownloads
Sub-componentDownload Bubble
Bug ClassLogic Error
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.
Tracker514023309
Fix commit3cfe51aa2838 (chromium/src) +37/-0
CWECWE-1021
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-05-17 (per vendor advisory)
Fix landed2026-07-30 (public git)
Disclosed2026-09-08
Patch gap40 days — fix public in source before the advisory

Background

Download bubble
the small popup panel anchored to the toolbar download icon that lists in-progress and completed downloads, one DownloadBubbleRowView per download.
Main page buttons
the primary text action buttons on a download row (e.g. KEEP, Open, Discard), tracked in DownloadBubbleRowView’s main_page_buttons_ map, as distinct from the row’s transparent_button and its quick_actions_ icon buttons.
Picture-in-picture (PiP) occlusion
the condition where a floating PiP video window overlaps the download bubble, surfaced to each row through DownloadBubbleRowView::OnOcclusionStateChanged(bool occluded).
Clickjacking
an attack in which an attacker-controlled overlay (here a PiP window) sits over a trusted UI control so the user is tricked into activating that control while believing they are interacting with the overlay.

Root Cause Analysis

DownloadBubbleRowView::OnOcclusionStateChanged() is the single place that reacts to a PiP window occluding the download bubble, and its job is to neutralize the row’s interactive controls so a user cannot be tricked into activating them through the overlay. Before the patch it disabled only the row’s transparent_button and every button in quick_actions_, but it left the main_page_buttons_ (the primary text actions such as KEEP) fully enabled and clickable. This violated the intended invariant that all actionable controls on a row are disabled while the row is occluded, leaving a clickjacking gap: a PiP window positioned over a main page button let a user’s click reach and activate that button (for example keeping/opening a dangerous download) while they believed they were clicking the video.

The fix adds a loop that calls SetEnabled(!occluded) on each entry of main_page_buttons_, giving the main page buttons exactly the same occlusion treatment as the other two control kinds and restoring the invariant. The accompanying GetMainPageButtonForTesting() accessor and the OcclusionDisablesButtons unit test lock in that all three button kinds toggle enabled state with occlusion.

Key insight
The core mistake was an incomplete occlusion handler: it disabled the transparent and quick-action buttons but forgot the main page buttons, so one class of clickable control remained active behind a PiP overlay. The fix closes the gap by disabling main_page_buttons_ alongside the others whenever occluded is true.

Attack Path

  1. Stage an occluding overlay An attacker gets the user to open a page that launches a picture-in-picture video window, which floats above other Chrome UI including the download bubble.
  2. Trigger a download bubble with an actionable row The attacker induces a download so the download bubble appears with a row whose main page button (e.g. KEEP) offers a security-relevant action on a suspicious file.
  3. Position the PiP window over the main page button The PiP window is moved so it overlaps the row; OnOcclusionStateChanged(true) fires and disables the transparent and quick-action buttons, but pre-patch leaves the main page button enabled and hittable.
  4. Harvest the click The user, believing they are interacting with the video, clicks through to the still-enabled main page button, activating the download action (such as keeping or opening the file) without realizing it. (inferred) — the diff shows the enabled control but does not itself demonstrate a specific end-to-end user interaction.

Impact Assessment

An attacker gains the ability to trick a user, in the browser UI of the trusted browser process, into activating a download-row main page button (such as keeping or opening a potentially dangerous download) they did not intend to, via a PiP overlay. Preconditions are that the user has an active download bubble row with a main page button and that a picture-in-picture window can be positioned over it. The advisory classifies this as a LogicError causing clickjacking in Downloads; any further consequence such as execution of a kept/opened file is a downstream user action, not something the patch demonstrates (inferred).

Changed Functions

FunctionChangeNotes
for
chrome/browser/ui/views/download/bubble/download_bubble_row_view.cc
modified
TEST_F
chrome/browser/ui/views/download/bubble/download_bubble_row_view_unittest.cc
modified

Files Changed

  • chrome/browser/ui/views/download/bubble/download_bubble_row_view.cc
  • chrome/browser/ui/views/download/bubble/download_bubble_row_view.h
  • chrome/browser/ui/views/download/bubble/download_bubble_row_view_unittest.cc

Audit Directions

  • Incomplete state fan-out across parallel control collections
    When one handler must apply a state change (enable/disable, visibility, focusability) to several sibling collections, audit that every collection is covered; here transparent_button and quick_actions_ were handled but main_page_buttons_ was missed.
  • Occlusion and input-protection handlers in bubbles/overlays
    Review other views reacting to PiP or window occlusion (and input-event activation protectors) to confirm all activatable controls, not just a subset, are neutralized while occluded.
  • New controls added after a security handler exists
    When a new button or control map is added to a row/view that already has occlusion or anti-clickjacking logic, verify the control is wired into that logic rather than silently left enabled.
From 3cfe51aa2838e8a95f78dbc9e82327de5db20870 Mon Sep 17 00:00:00 2001
From: Yaw Frempong <[email protected]>
Date: Thu, 30 Jul 2026 07:25:30 -0700
Subject: [PATCH] [Download Bubble] Disable download bubble main page buttons on PiP occlusion

DownloadBubbleRowView::OnOcclusionStateChanged() already disables the
row's transparent button and quick-action buttons while the bubble is
occluded by a picture-in-picture window. Apply the same treatment to the
main page buttons so that they behave consistently with the other
controls on the row, and add a unit test covering all three button
kinds.

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

Bug: 514023309
Change-Id: I3d27699cca9f8dc7010d18cdd09431927eb09753
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8169860
Commit-Queue: Yaw Frempong <[email protected]>
Reviewed-by: Lily Chen <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1671046}
---
diff --git a/chrome/browser/ui/views/download/bubble/download_bubble_row_view.cc b/chrome/browser/ui/views/download/bubble/download_bubble_row_view.cc
index a68d3391..f9b8c9c 100644
--- a/chrome/browser/ui/views/download/bubble/download_bubble_row_view.cc
+++ b/chrome/browser/ui/views/download/bubble/download_bubble_row_view.cc
@@ -978,6 +978,9 @@
   for (auto& [command, action_button] : quick_actions_) {
     action_button->SetEnabled(!occluded);
   }
+  for (auto& [command, main_page_button] : main_page_buttons_) {
+    main_page_button->SetEnabled(!occluded);
+  }
 }
 
 std::u16string_view DownloadBubbleRowView::GetSecondaryLabelTextForTesting() {
@@ -1053,6 +1056,13 @@
   return it->second;
 }
 
+views::MdTextButton* DownloadBubbleRowView::GetMainPageButtonForTesting(
+    DownloadCommands::Command command) {
+  auto it = main_page_buttons_.find(command);
+  CHECK(it != main_page_buttons_.end());
+  return it->second;
+}
+
 void DownloadBubbleRowView::SetInputProtectorForTesting(
     std::unique_ptr<views::InputEventActivationProtector> input_protector) {
   input_protector_ = std::move(input_protector);
diff --git a/chrome/browser/ui/views/download/bubble/download_bubble_row_view.h b/chrome/browser/ui/views/download/bubble/download_bubble_row_view.h
index 6ca1a625..141a7fe 100644
--- a/chrome/browser/ui/views/download/bubble/download_bubble_row_view.h
+++ b/chrome/browser/ui/views/download/bubble/download_bubble_row_view.h
@@ -112,6 +112,8 @@
   bool IsQuickActionButtonVisibleForTesting(DownloadCommands::Command command);
   views::ImageButton* GetQuickActionButtonForTesting(
       DownloadCommands::Command command);
+  views::MdTextButton* GetMainPageButtonForTesting(
+      DownloadCommands::Command command);
   void SetInputProtectorForTesting(
       std::unique_ptr<views::InputEventActivationProtector> input_protector);
 
diff --git a/chrome/browser/ui/views/download/bubble/download_bubble_row_view_unittest.cc b/chrome/browser/ui/views/download/bubble/download_bubble_row_view_unittest.cc
index 9fb3b9a..9b191dfd 100644
--- a/chrome/browser/ui/views/download/bubble/download_bubble_row_view_unittest.cc
+++ b/chrome/browser/ui/views/download/bubble/download_bubble_row_view_unittest.cc
@@ -26,6 +26,7 @@
 #include "ui/events/test/test_event.h"
 #include "ui/events/types/event_type.h"
 #include "ui/views/controls/button/image_button.h"
+#include "ui/views/controls/button/md_text_button.h"
 #include "ui/views/test/mock_input_event_activation_protector.h"
 
 namespace {
@@ -241,4 +242,28 @@
       ->OnMousePressed(event);
 }
 
+// Test that all button controls on the row are disabled while the row is
+// occluded by a picture-in-picture window, and re-enabled when it is not.
+TEST_F(DownloadBubbleRowViewTest, OcclusionDisablesButtons) {
+  views::Button* transparent_button = row_view()->transparent_button();
+  views::ImageButton* quick_action =
+      row_view()->GetQuickActionButtonForTesting(DownloadCommands::CANCEL);
+  views::MdTextButton* main_page_button =
+      row_view()->GetMainPageButtonForTesting(DownloadCommands::KEEP);
+
+  ASSERT_TRUE(transparent_button->GetEnabled());
+  ASSERT_TRUE(quick_action->GetEnabled());
+  ASSERT_TRUE(main_page_button->GetEnabled());
+
+  row_view()->OnOcclusionStateChanged(/*occluded=*/true);
+  EXPECT_FALSE(transparent_button->GetEnabled());
+  EXPECT_FALSE(quick_action->GetEnabled());
+  EXPECT_FALSE(main_page_button->GetEnabled());
+
+  row_view()->OnOcclusionStateChanged(/*occluded=*/false);
+  EXPECT_TRUE(transparent_button->GetEnabled());
+  EXPECT_TRUE(quick_action->GetEnabled());
+  EXPECT_TRUE(main_page_button->GetEnabled());
+}
+
 }  // namespace
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/views/download/bubble/download_bubble_row_view_unittest.cc b/chrome/browser/ui/views/download/bubble/download_bubble_row_view_unittest.cc
index 9fb3b9a..9b191dfd 100644
--- a/chrome/browser/ui/views/download/bubble/download_bubble_row_view_unittest.cc
+++ b/chrome/browser/ui/views/download/bubble/download_bubble_row_view_unittest.cc
@@ -26,6 +26,7 @@
 #include "ui/events/test/test_event.h"
 #include "ui/events/types/event_type.h"
 #include "ui/views/controls/button/image_button.h"
+#include "ui/views/controls/button/md_text_button.h"
 #include "ui/views/test/mock_input_event_activation_protector.h"
 
 namespace {
@@ -241,4 +242,28 @@
       ->OnMousePressed(event);
 }
 
+// Test that all button controls on the row are disabled while the row is
+// occluded by a picture-in-picture window, and re-enabled when it is not.
+TEST_F(DownloadBubbleRowViewTest, OcclusionDisablesButtons) {
+  views::Button* transparent_button = row_view()->transparent_button();
+  views::ImageButton* quick_action =
+      row_view()->GetQuickActionButtonForTesting(DownloadCommands::CANCEL);
+  views::MdTextButton* main_page_button =
+      row_view()->GetMainPageButtonForTesting(DownloadCommands::KEEP);
+
+  ASSERT_TRUE(transparent_button->GetEnabled());
+  ASSERT_TRUE(quick_action->GetEnabled());
+  ASSERT_TRUE(main_page_button->GetEnabled());
+
+  row_view()->OnOcclusionStateChanged(/*occluded=*/true);
+  EXPECT_FALSE(transparent_button->GetEnabled());
+  EXPECT_FALSE(quick_action->GetEnabled());
+  EXPECT_FALSE(main_page_button->GetEnabled());
+
+  row_view()->OnOcclusionStateChanged(/*occluded=*/false);
+  EXPECT_TRUE(transparent_button->GetEnabled());
+  EXPECT_TRUE(quick_action->GetEnabled());
+  EXPECT_TRUE(main_page_button->GetEnabled());
+}
+
 }  // namespace
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.