CVE-2026-2318
Overview
Background
- Download bubble row
DownloadBubbleRowViewis one entry in the toolbar download bubble; itstransparent_button_(activated by clicking the row) andquick_actions_buttons perform download actions such as opening or acting on the file.- Picture-in-Picture (PiP) window
- an always-on-top window (video or Document PiP) that a web page can open and populate, which can therefore sit above trusted browser UI.
- `PictureInPictureOcclusionObserver` / `ScopedPictureInPictureOcclusionObservation`
- an existing Chrome helper that tracks whether a given
views::Widgetis covered by a PiP window and callsOnOcclusionStateChanged(bool occluded)on the observer. - Clickjacking / input protection
- tricking a user into clicking security-sensitive UI they cannot properly see; the row already used
views::InputEventActivationProtector(input_protector_) to enforce a click delay, but that does not account for overlap with PiP windows.
Root Cause Analysis
Before this patch, DownloadBubbleRowView never consulted PiP occlusion, so its row-click and quick-action buttons stayed enabled even while a site-controlled, always-on-top PiP window covered the download bubble. The violated invariant is that security-sensitive browser UI should not accept activation while it is obscured by web-controlled content, which other Chrome surfaces enforce through the PiP occlusion tracker.
The fix makes DownloadBubbleRowView a PictureInPictureOcclusionObserver, adds a ScopedPictureInPictureOcclusionObservation pip_occlusion_observation_{this} member, starts observing the row’s widget with pip_occlusion_observation_.Observe(GetWidget()) when the view is attached to a widget, and in the new OnOcclusionStateChanged sets transparent_button_ and every quick_actions_ button to SetEnabled(!occluded).
As a result, the download actions are disabled for as long as a PiP window overlaps the bubble and re-enabled when it no longer does.
Attack Path
- Trigger a download a malicious page starts a download so the download bubble with its row and quick-action buttons is shown.
- Open a PiP window over the bubble the page opens a video or Document PiP window, which is always-on-top and positioned over or near the bubble (window placement details are not in the diff).
- Lure the user into clicking
the PiP content entices clicks so that a click lands on, or is timed to land on, the obscured
transparent_button_or aquick_actions_button (exact deception technique(inferred)). - Unintended download action
pre-patch, the still-enabled button executes the download action the user did not knowingly choose; post-patch,
OnOcclusionStateChanged(true)has disabled those buttons while occluded.
Impact Assessment
(inferred) and not shown by the patch.Changed Functions
| Function | Change | Notes |
|---|---|---|
forchrome/browser/ui/views/download/bubble/download_bubble_row_view.cc |
modified | |
DownloadBubbleRowViewchrome/browser/ui/views/download/bubble/download_bubble_row_view.h |
modified |
Files Changed
chrome/browser/ui/views/download/BUILD.gnchrome/browser/ui/views/download/bubble/download_bubble_row_view.ccchrome/browser/ui/views/download/bubble/download_bubble_row_view.h
Audit Directions
- Trusted UI without occlusion trackingenumerate other bubbles, prompts, and dialogs with consequential buttons (permission prompts, extension install, file-system access, payment, password bubbles) and confirm each observes PiP occlusion or equivalent.
- Enable-state clobbering
OnOcclusionStateChanged(false)unconditionally callsSetEnabled(true); check whether this can re-enable a quick action or row button that other logic had deliberately disabled, and whether a freshInputEventActivationProtectordelay applies after occlusion ends. - Observation lifetime across re-parentingverify
Observe(GetWidget())behaves correctly when the row is removed and re-added to widgets (e.g. bubble rebuilds orShowInactive()paths), so no window exists where the row is visible but unobserved.
Patch
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 5a45ce2..0f360ea5 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
@@ -165,6 +165,7 @@
focus_manager->AddFocusChangeListener(this);
RegisterAccelerators(focus_manager);
}
+ pip_occlusion_observation_.Observe(GetWidget());
}
void DownloadBubbleRowView::RemovedFromWidget() {
@@ -936,6 +937,13 @@
return focused;
}
+void DownloadBubbleRowView::OnOcclusionStateChanged(bool occluded) {
+ transparent_button_->SetEnabled(!occluded);
+ for (auto& [command, action_button] : quick_actions_) {
+ action_button->SetEnabled(!occluded);
+ }
+}
+
std::u16string_view DownloadBubbleRowView::GetSecondaryLabelTextForTesting() {
return secondary_label_->GetText();
}
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 0118c89..f593884 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
@@ -14,6 +14,8 @@
#include "base/timer/timer.h"
#include "chrome/browser/download/download_commands.h"
#include "chrome/browser/download/download_ui_model.h"
+#include "chrome/browser/picture_in_picture/picture_in_picture_occlusion_observer.h"
+#include "chrome/browser/picture_in_picture/scoped_picture_in_picture_occlusion_observation.h"
#include "chrome/browser/ui/download/download_bubble_row_view_info.h"
#include "chrome/browser/ui/download/download_item_mode.h"
#include "chrome/browser/ui/views/controls/hover_button.h"
@@ -46,6 +48,7 @@
class DownloadBubbleRowView : public views::View,
public views::ContextMenuController,
public views::FocusChangeListener,
+ public PictureInPictureOcclusionObserver,
public DownloadBubbleRowViewInfoObserver {
METADATA_HEADER(DownloadBubbleRowView, views::View)
@@ -92,6 +95,9 @@
bool AcceleratorPressed(const ui::Accelerator& accelerator) override;
bool CanHandleAccelerators() const override;
+ // PictureInPictureOcclusionObserver:
+ void OnOcclusionStateChanged(bool occluded) override;
+
// Returns the transparent button that is activated when the row is clicked.
views::Button* transparent_button() { return transparent_button_; }
@@ -244,6 +250,10 @@
// Mitigates the risk of clickjacking by enforcing a delay in click input.
std::unique_ptr<views::InputEventActivationProtector> input_protector_;
+ // Observes PiP windows in order to deactivate the button controls when
+ // occluded by a PiP, to prevent clickjacking.
+ ScopedPictureInPictureOcclusionObservation pip_occlusion_observation_{this};
+
// TODO(crbug.com/40233803): The size constraint is not passed down from the
// views tree in the first round of layout, so setting a fixed width to bound
// the view. This is assuming that the row view is loaded inside a bubble. It
diff --git a/chrome/browser/ui/views/download/BUILD.gn b/chrome/browser/ui/views/download/BUILD.gn
index 566d4863..1863fa6 100644
--- a/chrome/browser/ui/views/download/BUILD.gn
+++ b/chrome/browser/ui/views/download/BUILD.gn
@@ -68,6 +68,7 @@
"//chrome/app:command_ids",
"//chrome/browser:browser_process",
"//chrome/browser:primitives",
+ "//chrome/browser/picture_in_picture",
"//chrome/browser/profiles:profile",
"//chrome/browser/profiles:profile_util",
"//chrome/browser/safe_browsing",
Original Bug Report
User can unknowingly Execute External File Hidden behind PiP during Interaction
Security Bug
VULNERABILITY DETAILS An attacker can exploit a combination of opening a Picture-in-Picture (PiP) window and a hidden popup behind it. By manipulating the focus onto the concealed popup, an attacker can hijack keypresses when a user interacts with the website. This could lead to the unintended execution of a downloaded file. When the PiP window is closed on top of the downloaded file, the browser automatically shifts focus to this file, and with just two Enter key presses, the victim could unknowingly execute the file.
Using the method mentioned above, I have crafted an engaging game that requires the user to click on the PiP window to gather rewards and then asks the victim to press Enter twice to claim these rewards.
VERSION Chrome Version: 128.0.6613.114 (Official Build) (64-bit) Operating System: Windows 11
REPRODUCTION CASE
- Download the attached
poc.htmlfile. - Open the
poc.htmlfile in the latest Chromium browser. - Interact with the game and observe how the executable is successfully launched without the user’s awareness.
In this proof of concept, I used an example executable file from Sysinternals: adrestore.exe. However, this could be any arbitrary executable. The files are opened behind the PiP window, and when the user presses Enter twice, the first Enter closes the PiP window, and the second Enter opens the executable. This interaction can also occur without PiP, as any popup opened and closed in front of a downloaded file will automatically focus on the downloaded file, allowing the Enter key to initiate the launch of the file.
Please attach files directly, not in zip or other archive formats, and if you’ve created a demonstration site please also attach the files needed to reproduce the demonstration locally.
CREDIT INFORMATION Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: Shaheen Fazim