Chrome · Downloads
CVE-2026-87507
Logic Error in Downloads
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/download/bubble/download_bubble_security_view.cc |
modified | |
DownloadBubbleSecurityViewchrome/browser/ui/views/download/bubble/download_bubble_security_view.h |
modified |
Files Changed
chrome/browser/ui/views/download/bubble/download_bubble_security_view.ccchrome/browser/ui/views/download/bubble/download_bubble_security_view.hchrome/browser/ui/views/download/bubble/download_bubble_security_view_unittest.cc
Patch
From 6d86d50a2c19937cba6949c28dd2951b6ccc665a Mon Sep 17 00:00:00 2001 From: Yaw Frempong <[email protected]> Date: Mon, 10 Aug 2026 21:44:42 -0700 Subject: [PATCH] [Download Bubble] Disable subpage dialog buttons when occluded DownloadBubbleSecurityView wires its primary/secondary subpage buttons onto the bubble's DialogClientView OK/Cancel buttons. The earlier change that added picture-in-picture occlusion tracking to the download bubble only disabled the row-view controls; the dialog buttons used by the subpage were left enabled while the bubble is occluded. Mirror the row-view pattern: make DownloadBubbleSecurityView a PictureInPictureOcclusionObserver, observe the bubble widget in AddedToWidget(), and disable the dialog OK/Cancel buttons via DialogDelegate::SetButtonEnabled() while occluded. Also consult the occlusion state in UpdateButton() so re-populating the buttons does not re-enable them. Reviewed in https://crrev.com/i/9596093 Bug: 514009699 Change-Id: I7b01408059e9de9c8cc26bb0fa2f1e1a6c8e91f3 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8170820 Reviewed-by: Brian Lefler <[email protected]> Reviewed-by: Lily Chen <[email protected]> Commit-Queue: Yaw Frempong <[email protected]> Cr-Commit-Position: refs/heads/main@{#1676911} --- diff --git a/chrome/browser/ui/views/download/bubble/download_bubble_security_view.cc b/chrome/browser/ui/views/download/bubble/download_bubble_security_view.cc index 6270c8d..0853bdda 100644 --- a/chrome/browser/ui/views/download/bubble/download_bubble_security_view.cc +++ b/chrome/browser/ui/views/download/bubble/download_bubble_security_view.cc @@ -537,9 +537,10 @@ &HandleButtonClickWithDefaultClose, weak_factory_.GetWeakPtr(), button_info.command, is_secondary_button)); + bubble_delegate_->SetButtonEnabled(button_type, !occluded_); + if (button_type == ui::mojom::DialogButton::kCancel) { bubble_delegate_->SetCancelCallbackWithClose(callback); - bubble_delegate_->SetButtonEnabled(button_type, true); views::LabelButton* button = bubble_delegate_->GetCancelButton(); if (button_info.text_color) { button->SetEnabledTextColors(*button_info.text_color); @@ -688,6 +689,25 @@ DownloadBubbleSecurityView::~DownloadBubbleSecurityView() = default; +void DownloadBubbleSecurityView::AddedToWidget() { + views::Widget* widget = GetWidget(); + pip_occlusion_observation_.Observe(widget); +} + +void DownloadBubbleSecurityView::OnOcclusionStateChanged(bool occluded) { + if (occluded_ == occluded) { + return; + } + + // If transitioning from occluded to un-occluded, restart the input protection + // timer to prevent clickjacking/unintended clicks. + if (occluded_ && !occluded && bubble_delegate_) { + bubble_delegate_->TriggerInputProtection(); + } + occluded_ = occluded; + UpdateButtons(); +} + int DownloadBubbleSecurityView::GetMinimumBubbleWidth() const { return ChromeLayoutProvider::Get()->GetSnappedDialogWidth( bubble_delegate_->GetDialogClientView()->GetMinimumSize().width()); diff --git a/chrome/browser/ui/views/download/bubble/download_bubble_security_view.h b/chrome/browser/ui/views/download/bubble/download_bubble_security_view.h index 65b4902..62ffc349 100644 --- a/chrome/browser/ui/views/download/bubble/download_bubble_security_view.h +++ b/chrome/browser/ui/views/download/bubble/download_bubble_security_view.h @@ -14,6 +14,8 @@ #include "base/types/optional_ref.h" #include "chrome/browser/download/download_item_warning_data.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_security_view_info.h" #include "components/download/public/common/download_danger_type.h" #include "components/offline_items_collection/core/offline_item.h" @@ -34,6 +36,7 @@ class DownloadBubbleSecurityView : public views::View, + public PictureInPictureOcclusionObserver, public DownloadBubbleSecurityViewInfoObserver { METADATA_HEADER(DownloadBubbleSecurityView, views::View) @@ -90,6 +93,12 @@ delete; ~DownloadBubbleSecurityView() override; + // views::View: + void AddedToWidget() override; + + // PictureInPictureOcclusionObserver: + void OnOcclusionStateChanged(bool occluded) override; + // Whether this view is properly associated with a download. The rest of the // public method calls on this view do not make sense if not initialized. bool IsInitialized() const; @@ -184,6 +193,12 @@ // double-logging. bool did_log_action_ = false; + // Whether the bubble widget is currently occluded by a picture-in-picture + // window. The dialog buttons are disabled while occluded. + bool occluded_ = false; + + ScopedPictureInPictureOcclusionObservation pip_occlusion_observation_{this}; + base::WeakPtrFactory<DownloadBubbleSecurityView> weak_factory_{this}; }; diff --git a/chrome/browser/ui/views/download/bubble/download_bubble_security_view_unittest.cc b/chrome/browser/ui/views/download/bubble/download_bubble_security_view_unittest.cc index c1ecf56..6553966 100644 --- a/chrome/browser/ui/views/download/bubble/download_bubble_security_view_unittest.cc +++ b/chrome/browser/ui/views/download/bubble/download_bubble_security_view_unittest.cc @@ -14,6 +14,8 @@ #include "chrome/browser/download/download_ui_model.h" #include "chrome/browser/download/mock_download_core_service.h" #include "chrome/browser/download/offline_item_utils.h" +#include "chrome/browser/picture_in_picture/picture_in_picture_occlusion_tracker.h" +#include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h" #include "chrome/browser/ui/download/download_bubble_security_view_info.h" #include "chrome/browser/ui/views/chrome_layout_provider.h" #include "chrome/browser/ui/views/download/bubble/download_bubble_contents_view.h" @@ -31,6 +33,7 @@ #include "ui/base/mojom/dialog_button.mojom.h" #include "ui/color/color_id.h" #include "ui/views/bubble/bubble_dialog_delegate_view.h" +#include "ui/views/controls/button/md_text_button.h" #include "ui/views/view.h" #include "ui/views/window/dialog_client_view.h" @@ -166,15 +169,17 @@ bubble_delegate_ = bubble_delegate.get(); bubble_navigator_ = std::make_unique<MockDownloadBubbleNavigationHandler>( *security_view_info_); - views::BubbleDialogDelegate::CreateBubbleDeprecated( - std::move(bubble_delegate), - views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET); - bubble_delegate_->GetWidget()->Show(); + security_view_ = bubble_delegate_->SetContentsView( std::make_unique<DownloadBubbleSecurityView>( security_view_delegate_.get(), *security_view_info_, bubble_navigator_->GetWeakPtr(), bubble_delegate_)); + views::BubbleDialogDelegate::CreateBubbleDeprecated( + std::move(bubble_delegate), + views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET); + bubble_delegate_->GetWidget()->Show(); + DownloadCoreServiceFactory::GetInstance()->SetTestingFactory( profile_, base::BindRepeating(&BuildMockDownloadCoreService)); MockDownloadCoreService* mock_dcs = static_cast<MockDownloadCoreService*>( @@ -315,6 +320,46 @@ static_cast<int>(ui::mojom::DialogButton::kNone)); } +TEST_F(DownloadBubbleSecurityViewTest, + DialogButtonsDisabledWhenOccludedByPictureInPicture) { + security_view_info_->InitializeForDownload(*row1_model_); + security_view_info_->SetSubpageButtonsForTesting( + {SubpageButton(DownloadCommands::Command::DISCARD, std::u16string(), + /*is_prominent=*/true), + SubpageButton(DownloadCommands::Command::KEEP, std::u16string(), + /*is_prominent=*/false, ui::kColorAlertHighSeverity)}); + + ASSERT_NE(nullptr, bubble_delegate_->GetOkButton()); + ASSERT_NE(nullptr, bubble_delegate_->GetCancelButton()); + EXPECT_TRUE(bubble_delegate_->GetOkButton()->GetEnabled()); + EXPECT_TRUE(bubble_delegate_->GetCancelButton()->GetEnabled()); + + PictureInPictureOcclusionTracker* tracker = + PictureInPictureWindowManager::GetInstance()->GetOcclusionTracker(); + ASSERT_NE(nullptr, tracker); + tracker->SetWidgetOcclusionStateForTesting(bubble_delegate_->GetWidget(), + /*occluded=*/true); + + EXPECT_FALSE(bubble_delegate_->GetOkButton()->GetEnabled()); + EXPECT_FALSE(bubble_delegate_->GetCancelButton()->GetEnabled()); + + // Re-initializing the view while occluded must not re-enable the buttons. + security_view_info_->InitializeForDownload(*row1_model_); + security_view_info_->SetSubpageButtonsForTesting( + {SubpageButton(DownloadCommands::Command::DISCARD, std::u16string(), + /*is_prominent=*/true), + SubpageButton(DownloadCommands::Command::KEEP, std::u16string(), + /*is_prominent=*/false, ui::kColorAlertHighSeverity)}); + EXPECT_FALSE(bubble_delegate_->GetOkButton()->GetEnabled()); + EXPECT_FALSE(bubble_delegate_->GetCancelButton()->GetEnabled()); + + tracker->SetWidgetOcclusionStateForTesting(bubble_delegate_->GetWidget(), + /*occluded=*/false);
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/ui/views/download/bubble/download_bubble_security_view_unittest.cc b/chrome/browser/ui/views/download/bubble/download_bubble_security_view_unittest.cc
index c1ecf56..6553966 100644
--- a/chrome/browser/ui/views/download/bubble/download_bubble_security_view_unittest.cc
+++ b/chrome/browser/ui/views/download/bubble/download_bubble_security_view_unittest.cc
@@ -14,6 +14,8 @@
#include "chrome/browser/download/download_ui_model.h"
#include "chrome/browser/download/mock_download_core_service.h"
#include "chrome/browser/download/offline_item_utils.h"
+#include "chrome/browser/picture_in_picture/picture_in_picture_occlusion_tracker.h"
+#include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h"
#include "chrome/browser/ui/download/download_bubble_security_view_info.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/download/bubble/download_bubble_contents_view.h"
@@ -31,6 +33,7 @@
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/color/color_id.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
+#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/view.h"
#include "ui/views/window/dialog_client_view.h"
@@ -166,15 +169,17 @@
bubble_delegate_ = bubble_delegate.get();
bubble_navigator_ = std::make_unique<MockDownloadBubbleNavigationHandler>(
*security_view_info_);
- views::BubbleDialogDelegate::CreateBubbleDeprecated(
- std::move(bubble_delegate),
- views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET);
- bubble_delegate_->GetWidget()->Show();
+
security_view_ = bubble_delegate_->SetContentsView(
std::make_unique<DownloadBubbleSecurityView>(
security_view_delegate_.get(), *security_view_info_,
bubble_navigator_->GetWeakPtr(), bubble_delegate_));
+ views::BubbleDialogDelegate::CreateBubbleDeprecated(
+ std::move(bubble_delegate),
+ views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET);
+ bubble_delegate_->GetWidget()->Show();
+
DownloadCoreServiceFactory::GetInstance()->SetTestingFactory(
profile_, base::BindRepeating(&BuildMockDownloadCoreService));
MockDownloadCoreService* mock_dcs = static_cast<MockDownloadCoreService*>(
@@ -315,6 +320,46 @@
static_cast<int>(ui::mojom::DialogButton::kNone));
}
+TEST_F(DownloadBubbleSecurityViewTest,
+ DialogButtonsDisabledWhenOccludedByPictureInPicture) {
+ security_view_info_->InitializeForDownload(*row1_model_);
+ security_view_info_->SetSubpageButtonsForTesting(
+ {SubpageButton(DownloadCommands::Command::DISCARD, std::u16string(),
+ /*is_prominent=*/true),
+ SubpageButton(DownloadCommands::Command::KEEP, std::u16string(),
+ /*is_prominent=*/false, ui::kColorAlertHighSeverity)});
+
+ ASSERT_NE(nullptr, bubble_delegate_->GetOkButton());
+ ASSERT_NE(nullptr, bubble_delegate_->GetCancelButton());
+ EXPECT_TRUE(bubble_delegate_->GetOkButton()->GetEnabled());
+ EXPECT_TRUE(bubble_delegate_->GetCancelButton()->GetEnabled());
+
+ PictureInPictureOcclusionTracker* tracker =
+ PictureInPictureWindowManager::GetInstance()->GetOcclusionTracker();
+ ASSERT_NE(nullptr, tracker);
+ tracker->SetWidgetOcclusionStateForTesting(bubble_delegate_->GetWidget(),
+ /*occluded=*/true);
+
+ EXPECT_FALSE(bubble_delegate_->GetOkButton()->GetEnabled());
+ EXPECT_FALSE(bubble_delegate_->GetCancelButton()->GetEnabled());
+
+ // Re-initializing the view while occluded must not re-enable the buttons.
+ security_view_info_->InitializeForDownload(*row1_model_);
+ security_view_info_->SetSubpageButtonsForTesting(
+ {SubpageButton(DownloadCommands::Command::DISCARD, std::u16string(),
+ /*is_prominent=*/true),
+ SubpageButton(DownloadCommands::Command::KEEP, std::u16string(),
+ /*is_prominent=*/false, ui::kColorAlertHighSeverity)});
+ EXPECT_FALSE(bubble_delegate_->GetOkButton()->GetEnabled());
+ EXPECT_FALSE(bubble_delegate_->GetCancelButton()->GetEnabled());
+
+ tracker->SetWidgetOcclusionStateForTesting(bubble_delegate_->GetWidget(),
+ /*occluded=*/false);
+
+ EXPECT_TRUE(bubble_delegate_->GetOkButton()->GetEnabled());
+ EXPECT_TRUE(bubble_delegate_->GetCancelButton()->GetEnabled());
+}
+
TEST_F(DownloadBubbleSecurityViewTest, VerifyLogWarningActions) {
DownloadItemWarningData::AddWarningActionEvent(
&download_item1_, WarningSurface::BUBBLE_MAINPAGE, WarningAction::SHOWN);
@@ -501,7 +546,8 @@
security_view_info_->InitializeForDownload(*row1_model_);
security_view_info_->SetSubpageButtonsForTesting({SubpageButton(
DownloadCommands::Command::DISCARD,
- std::u16string(u"really really really really really really long "
+ std::u16string(u"really really really really really really really "
+ u"really really really really really really long "
u"button text"),
/*is_prominent=*/true)});
UpdateView();
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.
References
On This Page