Chrome · FileHandling
CVE-2026-87473
Logic Error in FileHandling
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
IN_PROC_BROWSER_TEST_Fchrome/browser/ui/views/web_apps/file_handler_launch_dialog_browsertest.cc |
modified |
Files Changed
chrome/browser/ui/views/web_apps/file_handler_launch_dialog_browsertest.ccchrome/browser/ui/views/web_apps/launch_app_user_choice_dialog_view.ccchrome/browser/ui/views/web_apps/launch_app_user_choice_dialog_view.hchrome/browser/ui/views/web_apps/protocol_handler_launch_dialog_browsertest.ccchrome/browser/ui/views/web_apps/protocol_handler_launch_dialog_view.cc
Patch
From a245abe0732f3d9f180163b2de0ab52f54ec59c1 Mon Sep 17 00:00:00 2001
From: Kunjan Vinay Patel <ksukh@google.com>
Date: Mon, 10 Aug 2026 14:31:48 -0700
Subject: [PATCH] [PWA] Prevent keyjacking on web app launch consent dialogs.
This change enhances security for web app launch dialogs (covering both
File and Protocol handlers) to prevent accidental or malicious bypasses
via rapid key presses:
- Sets the default focused button to 'Cancel' in the base class
LaunchAppUserChoiceDialogView.
- Enforces the 500ms input activation delay for keyboard events by
overriding ShouldAllowKeyEventsDuringInputProtection() to return
false.
Previously, these dialogs could be immediately accepted via the 'Enter'
key because the default button was 'OK' (specifically for File Handlers)
and key events were not blocked during the initial activation delay.
Bug: 537101736
Change-Id: Ie7396ee149ca498e0df19441c548c8eaa22e9d42
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8226085
Reviewed-by: Dibyajyoti Pal <dibyapal@chromium.org>
Commit-Queue: Kunjan Patel <ksukh@chromium.org>
Reviewed-by: Luke Deng <lukedeng@google.com>
Cr-Commit-Position: refs/heads/main@{#1676726}
---
diff --git a/chrome/browser/ui/views/web_apps/file_handler_launch_dialog_browsertest.cc b/chrome/browser/ui/views/web_apps/file_handler_launch_dialog_browsertest.cc
index 6aa7bdb2..0af938e 100644
--- a/chrome/browser/ui/views/web_apps/file_handler_launch_dialog_browsertest.cc
+++ b/chrome/browser/ui/views/web_apps/file_handler_launch_dialog_browsertest.cc
@@ -226,6 +226,24 @@
EXPECT_EQ(1U, GlobalBrowserCollection::GetInstance()->GetSize());
}
+IN_PROC_BROWSER_TEST_F(FileHandlerLaunchDialogTest,
+ DefaultButtonAndInputProtection) {
+ views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{},
+ "FileHandlerLaunchDialogView");
+ LaunchAppWithFiles({{base::FilePath::FromASCII("foo.txt")}});
+ views::Widget* widget = waiter.WaitIfNeededAndGet();
+ ASSERT_NE(widget, nullptr);
+ views::DialogDelegate* dialog_delegate =
+ widget->widget_delegate()->AsDialogDelegate();
+ ASSERT_NE(dialog_delegate, nullptr);
+
+ EXPECT_EQ(dialog_delegate->GetDefaultDialogButton(),
+ static_cast<int>(ui::mojom::DialogButton::kCancel));
+ EXPECT_FALSE(dialog_delegate->ShouldAllowKeyEventsDuringInputProtection());
+
+ views::test::CancelDialog(widget);
+}
+
IN_PROC_BROWSER_TEST_F(FileHandlerLaunchDialogTest, DisallowAndRemember) {
// One normal browser window exists.
EXPECT_EQ(1U, GlobalBrowserCollection::GetInstance()->GetSize());
diff --git a/chrome/browser/ui/views/web_apps/launch_app_user_choice_dialog_view.cc b/chrome/browser/ui/views/web_apps/launch_app_user_choice_dialog_view.cc
index 4141892..003b136 100644
--- a/chrome/browser/ui/views/web_apps/launch_app_user_choice_dialog_view.cc
+++ b/chrome/browser/ui/views/web_apps/launch_app_user_choice_dialog_view.cc
@@ -83,6 +83,8 @@
SetCanResize(false);
set_draggable(true);
+ SetDefaultButton(static_cast<int>(ui::mojom::DialogButton::kCancel));
+
SetAcceptCallback(base::BindOnce(&LaunchAppUserChoiceDialogView::OnAccepted,
base::Unretained(this)));
@@ -233,6 +235,11 @@
gfx::ImageSkia::CreateFrom1xBitmap(std::move(masked_bitmap))));
}
+bool LaunchAppUserChoiceDialogView::ShouldAllowKeyEventsDuringInputProtection()
+ const {
+ return false;
+}
+
BEGIN_METADATA(LaunchAppUserChoiceDialogView)
END_METADATA
diff --git a/chrome/browser/ui/views/web_apps/launch_app_user_choice_dialog_view.h b/chrome/browser/ui/views/web_apps/launch_app_user_choice_dialog_view.h
index 406708d3..7c164a5b 100644
--- a/chrome/browser/ui/views/web_apps/launch_app_user_choice_dialog_view.h
+++ b/chrome/browser/ui/views/web_apps/launch_app_user_choice_dialog_view.h
@@ -46,6 +46,9 @@
void Init();
+ // views::DialogDelegate:
+ bool ShouldAllowKeyEventsDuringInputProtection() const override;
+
static void SetDefaultRememberSelectionForTesting(bool remember_selection);
protected:
diff --git a/chrome/browser/ui/views/web_apps/protocol_handler_launch_dialog_browsertest.cc b/chrome/browser/ui/views/web_apps/protocol_handler_launch_dialog_browsertest.cc
index 868700c..00c5c32 100644
--- a/chrome/browser/ui/views/web_apps/protocol_handler_launch_dialog_browsertest.cc
+++ b/chrome/browser/ui/views/web_apps/protocol_handler_launch_dialog_browsertest.cc
@@ -108,6 +108,29 @@
}
IN_PROC_BROWSER_TEST_F(ProtocolHandlerLaunchDialogBrowserTest,
+ DefaultButtonAndInputProtection) {
+ views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{},
+ "ProtocolHandlerLaunchDialogView");
+ GURL protocol_url("web+test://test");
+ webapps::AppId test_app_id = InstallTestWebApp(browser()->GetProfile());
+
+ ShowWebAppProtocolLaunchDialog(protocol_url, browser()->GetProfile(),
+ test_app_id, base::DoNothing());
+
+ views::Widget* widget = waiter.WaitIfNeededAndGet();
+ ASSERT_NE(widget, nullptr);
+ views::DialogDelegate* dialog_delegate =
+ widget->widget_delegate()->AsDialogDelegate();
+ ASSERT_NE(dialog_delegate, nullptr);
+
+ EXPECT_EQ(dialog_delegate->GetDefaultDialogButton(),
+ static_cast<int>(ui::mojom::DialogButton::kCancel));
+ EXPECT_FALSE(dialog_delegate->ShouldAllowKeyEventsDuringInputProtection());
+
+ views::test::CancelDialog(widget);
+}
+
+IN_PROC_BROWSER_TEST_F(ProtocolHandlerLaunchDialogBrowserTest,
ProtocolHandlerIntentPickerDialog_DisallowAndRemember) {
ProtocolHandlerLaunchDialogView::SetDefaultRememberSelectionForTesting(true);
ShowDialogAndCloseWithReason(
diff --git a/chrome/browser/ui/views/web_apps/protocol_handler_launch_dialog_view.cc b/chrome/browser/ui/views/web_apps/protocol_handler_launch_dialog_view.cc
index 94f1812..32b758e 100644
--- a/chrome/browser/ui/views/web_apps/protocol_handler_launch_dialog_view.cc
+++ b/chrome/browser/ui/views/web_apps/protocol_handler_launch_dialog_view.cc
@@ -39,7 +39,6 @@
SetButtonLabel(
ui::mojom::DialogButton::kCancel,
l10n_util::GetStringUTF16(IDS_WEB_APP_PERMISSION_NEGATIVE_BUTTON));
- SetDefaultButton(static_cast<int>(ui::mojom::DialogButton::kCancel));
}
ProtocolHandlerLaunchDialogView::~ProtocolHandlerLaunchDialogView() = default;
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/ui/views/web_apps/file_handler_launch_dialog_browsertest.cc b/chrome/browser/ui/views/web_apps/file_handler_launch_dialog_browsertest.cc
index 6aa7bdb2..0af938e 100644
--- a/chrome/browser/ui/views/web_apps/file_handler_launch_dialog_browsertest.cc
+++ b/chrome/browser/ui/views/web_apps/file_handler_launch_dialog_browsertest.cc
@@ -226,6 +226,24 @@
EXPECT_EQ(1U, GlobalBrowserCollection::GetInstance()->GetSize());
}
+IN_PROC_BROWSER_TEST_F(FileHandlerLaunchDialogTest,
+ DefaultButtonAndInputProtection) {
+ views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{},
+ "FileHandlerLaunchDialogView");
+ LaunchAppWithFiles({{base::FilePath::FromASCII("foo.txt")}});
+ views::Widget* widget = waiter.WaitIfNeededAndGet();
+ ASSERT_NE(widget, nullptr);
+ views::DialogDelegate* dialog_delegate =
+ widget->widget_delegate()->AsDialogDelegate();
+ ASSERT_NE(dialog_delegate, nullptr);
+
+ EXPECT_EQ(dialog_delegate->GetDefaultDialogButton(),
+ static_cast<int>(ui::mojom::DialogButton::kCancel));
+ EXPECT_FALSE(dialog_delegate->ShouldAllowKeyEventsDuringInputProtection());
+
+ views::test::CancelDialog(widget);
+}
+
IN_PROC_BROWSER_TEST_F(FileHandlerLaunchDialogTest, DisallowAndRemember) {
// One normal browser window exists.
EXPECT_EQ(1U, GlobalBrowserCollection::GetInstance()->GetSize());
diff --git a/chrome/browser/ui/views/web_apps/protocol_handler_launch_dialog_browsertest.cc b/chrome/browser/ui/views/web_apps/protocol_handler_launch_dialog_browsertest.cc
index 868700c..00c5c32 100644
--- a/chrome/browser/ui/views/web_apps/protocol_handler_launch_dialog_browsertest.cc
+++ b/chrome/browser/ui/views/web_apps/protocol_handler_launch_dialog_browsertest.cc
@@ -108,6 +108,29 @@
}
IN_PROC_BROWSER_TEST_F(ProtocolHandlerLaunchDialogBrowserTest,
+ DefaultButtonAndInputProtection) {
+ views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{},
+ "ProtocolHandlerLaunchDialogView");
+ GURL protocol_url("web+test://test");
+ webapps::AppId test_app_id = InstallTestWebApp(browser()->GetProfile());
+
+ ShowWebAppProtocolLaunchDialog(protocol_url, browser()->GetProfile(),
+ test_app_id, base::DoNothing());
+
+ views::Widget* widget = waiter.WaitIfNeededAndGet();
+ ASSERT_NE(widget, nullptr);
+ views::DialogDelegate* dialog_delegate =
+ widget->widget_delegate()->AsDialogDelegate();
+ ASSERT_NE(dialog_delegate, nullptr);
+
+ EXPECT_EQ(dialog_delegate->GetDefaultDialogButton(),
+ static_cast<int>(ui::mojom::DialogButton::kCancel));
+ EXPECT_FALSE(dialog_delegate->ShouldAllowKeyEventsDuringInputProtection());
+
+ views::test::CancelDialog(widget);
+}
+
+IN_PROC_BROWSER_TEST_F(ProtocolHandlerLaunchDialogBrowserTest,
ProtocolHandlerIntentPickerDialog_DisallowAndRemember) {
ProtocolHandlerLaunchDialogView::SetDefaultRememberSelectionForTesting(true);
ShowDialogAndCloseWithReason(
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