Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Views
DescriptionMissing authorization in Views
ComponentViews
Bug ClassLogic Error
Tracker523277481
Fix commit0bdba256831a (chromium/src) +283/-9
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
TEST_F
chrome/browser/ui/file_system_access/file_system_access_permission_dialog_unittest.cc
modified

Files Changed

  • chrome/browser/ui/file_system_access/file_system_access_permission_dialog.cc
  • chrome/browser/ui/file_system_access/file_system_access_permission_dialog_unittest.cc
  • chrome/browser/ui/views/web_dialog_view_interactive_uitest.cc
  • chrome/test/BUILD.gn
  • ui/base/test/test_dialog_model_host.cc
  • ui/base/test/test_dialog_model_host.h
  • ui/views/controls/webview/unhandled_keyboard_event_handler.cc
From 0bdba256831acfd7e96b8cce9746becfcd0deae5 Mon Sep 17 00:00:00 2001
From: Mingyu Lei <[email protected]>
Date: Tue, 04 Aug 2026 20:34:14 -0700
Subject: [PATCH] Check focused view before redispatching unhandled keys for dialog

`UnhandledKeyboardEventHandler` forwards keyboard events that the
renderer did not consume directly to
`FocusManager::ProcessAccelerator()`. Unlike
`FocusManager::OnKeyEvent()`, this path did not check whether the
currently focused view wants to handle the key itself via
`SkipDefaultKeyEventProcessing()`, so a focused non-default button did
not get the chance to claim the Enter key before the default button's
accelerator fired.

In addition, dialogs built with `ui::DialogModel` default to
`enable_input_protection_ = false`, allowing key events to be accepted
immediately after a dialog appears. Combined, an unconsumed Enter
keypress from web contents could bypass a focused Cancel button and
trigger the default Allow button on sensitive tab-modal prompts such as
the File System Access permission dialog.

This CL explicitly enables input protection on
`FileSystemAccessPermissionDialog` to protect against rapid or
accidental keystrokes.

Bug: 523277481
Change-Id: I153353c3b8d80f17d42e432fb10ec888f1d284de
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8166200
Commit-Queue: Mingyu Lei <[email protected]>
Reviewed-by: Dana Fried <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1673874}
---

diff --git a/chrome/browser/ui/file_system_access/file_system_access_permission_dialog.cc b/chrome/browser/ui/file_system_access/file_system_access_permission_dialog.cc
index 43ee10c..e5cc182c6 100644
--- a/chrome/browser/ui/file_system_access/file_system_access_permission_dialog.cc
+++ b/chrome/browser/ui/file_system_access/file_system_access_permission_dialog.cc
@@ -166,7 +166,8 @@
                            .SetLabel(l10n_util::GetStringUTF16(
                                GetCancelButtonLabel(file_request_data))))
       .SetCloseActionCallback(std::move(cancel_callbacks.second))
-      .SetInitiallyFocusedField(kCancelButtonId);
+      .SetInitiallyFocusedField(kCancelButtonId)
+      .SetEnableInputProtection(true);
   return dialog_builder.Build();
 }
 
diff --git a/chrome/browser/ui/file_system_access/file_system_access_permission_dialog_unittest.cc b/chrome/browser/ui/file_system_access/file_system_access_permission_dialog_unittest.cc
index 7131f04..9349a38b 100644
--- a/chrome/browser/ui/file_system_access/file_system_access_permission_dialog_unittest.cc
+++ b/chrome/browser/ui/file_system_access/file_system_access_permission_dialog_unittest.cc
@@ -97,3 +97,12 @@
   EXPECT_EQ(host->GetInitiallyFocusedField(),
             host->GetId(ui::TestDialogModelHost::ButtonId::kCancel));
 }
+
+// Verifies that input protection is enabled to guard against accidental
+// or rapid-interaction keystrokes when the dialog appears.
+TEST_F(FileSystemAccessPermissionDialogTest, EnableInputProtection) {
+  TestFileSystemAccessPermissionDialog test_dialog;
+  auto host = test_dialog.CreateDialogModelHost();
+
+  EXPECT_TRUE(host->GetEnableInputProtection());
+}
diff --git a/chrome/browser/ui/views/web_dialog_view_interactive_uitest.cc b/chrome/browser/ui/views/web_dialog_view_interactive_uitest.cc
index c7aacefb..6c489f4 100644
--- a/chrome/browser/ui/views/web_dialog_view_interactive_uitest.cc
+++ b/chrome/browser/ui/views/web_dialog_view_interactive_uitest.cc
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "ui/views/controls/webview/web_dialog_view.h"
+
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
 #include "base/location.h"
@@ -20,13 +22,18 @@
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/interactive_test_utils.h"
 #include "chrome/test/base/ui_test_utils.h"
+#include "components/input/native_web_keyboard_event.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/render_widget_host_view.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_test.h"
-#include "content/public/test/test_utils.h"
+#include "third_party/blink/public/common/input/web_input_event.h"
+#include "ui/base/accelerators/accelerator.h"
+#include "ui/base/accelerators/test_accelerator_target.h"
 #include "ui/base/mojom/ui_base_types.mojom-shared.h"
-#include "ui/views/controls/webview/web_dialog_view.h"
+#include "ui/events/keycodes/keyboard_codes.h"
+#include "ui/views/controls/button/label_button.h"
+#include "ui/views/focus/focus_manager.h"
 #include "ui/views/view_tracker.h"
 #include "ui/views/widget/widget.h"
 #include "ui/views/widget/widget_observer.h"
@@ -320,6 +327,44 @@
   EXPECT_FALSE(was_view_deleted());
 }
 
+// Verifies that when focus has switched to a button that consumes key events
+// (such as a Confirm button), an unhandled Enter key event bubbling up from
+// WebContents does not trigger an Enter accelerator.
+// This is the regression test for crbug.com/523277481.
+IN_PROC_BROWSER_TEST_F(WebDialogBrowserTest,
+                       UnhandledEnterRespectsButtonFocus) {
+  views::FocusManager* focus_manager = view_->GetFocusManager();
+  ASSERT_TRUE(focus_manager);
+
+  ui::TestAcceleratorTarget target;
+  focus_manager->RegisterAccelerator(
+      ui::Accelerator(ui::VKEY_RETURN, ui::EF_NONE),
+      ui::AcceleratorManager::kNormalPriority, &target);
+
+  input::NativeWebKeyboardEvent event(
+      blink::WebInputEvent::Type::kRawKeyDown,
+      blink::WebInputEvent::kNoModifiers,
+      blink::WebInputEvent::GetStaticTimeStampForTests());
+  event.windows_key_code = ui::VKEY_RETURN;
+
+  // When WebDialogView is focused, an unhandled Enter event fires the
+  // accelerator.
+  focus_manager->SetFocusedView(view_);
+  EXPECT_EQ(view_, focus_manager->GetFocusedView());
+  view_->HandleKeyboardEvent(view_->web_contents(), event);
+  EXPECT_EQ(1, target.accelerator_count());
+
+  // After switching focus to a default button, an unhandled Enter event does
+  // not fire the accelerator.
+  auto* button = view_->AddChildView(std::make_unique<views::LabelButton>(
+      views::Button::PressedCallback(), u"Confirm"));
+  button->SetIsDefault(true);
+  focus_manager->SetFocusedView(button);
+  EXPECT_EQ(button, focus_manager->GetFocusedView());
+  view_->HandleKeyboardEvent(view_->web_contents(), event);
+  EXPECT_EQ(1, target.accelerator_count());
+}
+
 // Test that key event is translated to a text input properly.
 // TODO(crbug.com/500602996): Enable the test.
 #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 5a3736e..189b503 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -7723,6 +7723,7 @@
     }
 
     sources += [
+      "../../ui/views/controls/webview/unhandled_keyboard_event_handler_unittest.cc",
       "../../ui/views/controls/webview/web_dialog_view_unittest.cc",
       "../../ui/views/controls/webview/webview_unittest.cc",
       "../browser/ui/views/autofill/popup/lazy_loading_image_view_unittest.cc",
diff --git a/ui/base/test/test_dialog_model_host.cc b/ui/base/test/test_dialog_model_host.cc
index 077cb3d4..ea7ad684 100644
--- a/ui/base/test/test_dialog_model_host.cc
+++ b/ui/base/test/test_dialog_model_host.cc
@@ -99,6 +99,10 @@
   return dialog_model_->initially_focused_field(DialogModelHost::GetPassKey());
 }
 
+bool TestDialogModelHost::GetEnableInputProtection() {
+  return dialog_model_->enable_input_protection(DialogModelHost::GetPassKey());
+}
+
 void TestDialogModelHost::Close() {
   // For now, TestDialogModelHost::Close() is the expected interface to close.
   NOTREACHED();
diff --git a/ui/base/test/test_dialog_model_host.h b/ui/base/test/test_dialog_model_host.h
index e4654d0c..66047ab6 100644
--- a/ui/base/test/test_dialog_model_host.h
+++ b/ui/base/test/test_dialog_model_host.h
@@ -51,6 +51,7 @@
   const std::u16string& GetLabel(ButtonId button_id);
   ElementIdentifier GetId(ButtonId button_id);
   ElementIdentifier GetInitiallyFocusedField();
+  bool GetEnableInputProtection();
 
  private:
   // DialogModelHost:
diff --git a/ui/views/controls/webview/unhandled_keyboard_event_handler.cc b/ui/views/controls/webview/unhandled_keyboard_event_handler.cc
index 5caf765c..7e02d48 100644
--- a/ui/views/controls/webview/unhandled_keyboard_event_handler.cc
+++ b/ui/views/controls/webview/unhandled_keyboard_event_handler.cc
@@ -6,7 +6,9 @@
 
 #include "components/input/native_web_keyboard_event.h"
 #include "ui/content_accelerators/accelerator_util.h"
+#include "ui/views/controls/webview/webview.h"
 #include "ui/views/focus/focus_manager.h"
+#include "ui/views/view_utils.h"
 
 namespace views {
 
@@ -31,10 +33,22 @@
   // always generate a Char event.
   ignore_next_char_event_ = false;
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/file_system_access/file_system_access_permission_dialog_unittest.cc b/chrome/browser/ui/file_system_access/file_system_access_permission_dialog_unittest.cc
index 7131f04..9349a38b 100644
--- a/chrome/browser/ui/file_system_access/file_system_access_permission_dialog_unittest.cc
+++ b/chrome/browser/ui/file_system_access/file_system_access_permission_dialog_unittest.cc
@@ -97,3 +97,12 @@
   EXPECT_EQ(host->GetInitiallyFocusedField(),
             host->GetId(ui::TestDialogModelHost::ButtonId::kCancel));
 }
+
+// Verifies that input protection is enabled to guard against accidental
+// or rapid-interaction keystrokes when the dialog appears.
+TEST_F(FileSystemAccessPermissionDialogTest, EnableInputProtection) {
+  TestFileSystemAccessPermissionDialog test_dialog;
+  auto host = test_dialog.CreateDialogModelHost();
+
+  EXPECT_TRUE(host->GetEnableInputProtection());
+}
diff --git a/chrome/browser/ui/views/web_dialog_view_interactive_uitest.cc b/chrome/browser/ui/views/web_dialog_view_interactive_uitest.cc
index c7aacefb..6c489f4 100644
--- a/chrome/browser/ui/views/web_dialog_view_interactive_uitest.cc
+++ b/chrome/browser/ui/views/web_dialog_view_interactive_uitest.cc
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "ui/views/controls/webview/web_dialog_view.h"
+
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
 #include "base/location.h"
@@ -20,13 +22,18 @@
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/interactive_test_utils.h"
 #include "chrome/test/base/ui_test_utils.h"
+#include "components/input/native_web_keyboard_event.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/render_widget_host_view.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_test.h"
-#include "content/public/test/test_utils.h"
+#include "third_party/blink/public/common/input/web_input_event.h"
+#include "ui/base/accelerators/accelerator.h"
+#include "ui/base/accelerators/test_accelerator_target.h"
 #include "ui/base/mojom/ui_base_types.mojom-shared.h"
-#include "ui/views/controls/webview/web_dialog_view.h"
+#include "ui/events/keycodes/keyboard_codes.h"
+#include "ui/views/controls/button/label_button.h"
+#include "ui/views/focus/focus_manager.h"
 #include "ui/views/view_tracker.h"
 #include "ui/views/widget/widget.h"
 #include "ui/views/widget/widget_observer.h"
@@ -320,6 +327,44 @@
   EXPECT_FALSE(was_view_deleted());
 }
 
+// Verifies that when focus has switched to a button that consumes key events
+// (such as a Confirm button), an unhandled Enter key event bubbling up from
+// WebContents does not trigger an Enter accelerator.
+// This is the regression test for crbug.com/523277481.
+IN_PROC_BROWSER_TEST_F(WebDialogBrowserTest,
+                       UnhandledEnterRespectsButtonFocus) {
+  views::FocusManager* focus_manager = view_->GetFocusManager();
+  ASSERT_TRUE(focus_manager);
+
+  ui::TestAcceleratorTarget target;
+  focus_manager->RegisterAccelerator(
+      ui::Accelerator(ui::VKEY_RETURN, ui::EF_NONE),
+      ui::AcceleratorManager::kNormalPriority, &target);
+
+  input::NativeWebKeyboardEvent event(
+      blink::WebInputEvent::Type::kRawKeyDown,
+      blink::WebInputEvent::kNoModifiers,
+      blink::WebInputEvent::GetStaticTimeStampForTests());
+  event.windows_key_code = ui::VKEY_RETURN;
+
+  // When WebDialogView is focused, an unhandled Enter event fires the
+  // accelerator.
+  focus_manager->SetFocusedView(view_);
+  EXPECT_EQ(view_, focus_manager->GetFocusedView());
+  view_->HandleKeyboardEvent(view_->web_contents(), event);
+  EXPECT_EQ(1, target.accelerator_count());
+
+  // After switching focus to a default button, an unhandled Enter event does
+  // not fire the accelerator.
+  auto* button = view_->AddChildView(std::make_unique<views::LabelButton>(
+      views::Button::PressedCallback(), u"Confirm"));
+  button->SetIsDefault(true);
+  focus_manager->SetFocusedView(button);
+  EXPECT_EQ(button, focus_manager->GetFocusedView());
+  view_->HandleKeyboardEvent(view_->web_contents(), event);
+  EXPECT_EQ(1, target.accelerator_count());
+}
+
 // Test that key event is translated to a text input properly.
 // TODO(crbug.com/500602996): Enable the test.
 #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 5a3736e..189b503 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -7723,6 +7723,7 @@
     }
 
     sources += [
+      "../../ui/views/controls/webview/unhandled_keyboard_event_handler_unittest.cc",
       "../../ui/views/controls/webview/web_dialog_view_unittest.cc",
       "../../ui/views/controls/webview/webview_unittest.cc",
       "../browser/ui/views/autofill/popup/lazy_loading_image_view_unittest.cc",
diff --git a/ui/base/test/test_dialog_model_host.cc b/ui/base/test/test_dialog_model_host.cc
index 077cb3d4..ea7ad684 100644
--- a/ui/base/test/test_dialog_model_host.cc
+++ b/ui/base/test/test_dialog_model_host.cc
@@ -99,6 +99,10 @@
   return dialog_model_->initially_focused_field(DialogModelHost::GetPassKey());
 }
 
+bool TestDialogModelHost::GetEnableInputProtection() {
+  return dialog_model_->enable_input_protection(DialogModelHost::GetPassKey());
+}
+
 void TestDialogModelHost::Close() {
   // For now, TestDialogModelHost::Close() is the expected interface to close.
   NOTREACHED();
diff --git a/ui/base/test/test_dialog_model_host.h b/ui/base/test/test_dialog_model_host.h
index e4654d0c..66047ab6 100644
--- a/ui/base/test/test_dialog_model_host.h
+++ b/ui/base/test/test_dialog_model_host.h
@@ -51,6 +51,7 @@
   const std::u16string& GetLabel(ButtonId button_id);
   ElementIdentifier GetId(ButtonId button_id);
   ElementIdentifier GetInitiallyFocusedField();
+  bool GetEnableInputProtection();
 
  private:
   // DialogModelHost:
diff --git a/ui/views/controls/webview/unhandled_keyboard_event_handler_unittest.cc b/ui/views/controls/webview/unhandled_keyboard_event_handler_unittest.cc
new file mode 100644
index 0000000..ae4a3af
--- /dev/null
+++ b/ui/views/controls/webview/unhandled_keyboard_event_handler_unittest.cc
@@ -0,0 +1,186 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/views/controls/webview/unhandled_keyboard_event_handler.h"
+
+#include <memory>
+
+#include "base/functional/bind.h"
+#include "base/memory/raw_ptr.h"
+#include "base/test/task_environment.h"
+#include "components/input/native_web_keyboard_event.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/common/content_client.h"
+#include "content/public/test/browser_task_environment.h"
+#include "content/public/test/test_browser_context.h"
+#include "content/public/test/test_content_browser_client.h"
+#include "content/public/test/test_renderer_host.h"
+#include "content/public/test/web_contents_tester.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/base/accelerators/accelerator.h"
+#include "ui/base/accelerators/accelerator_manager.h"
+#include "ui/base/accelerators/test_accelerator_target.h"
+#include "ui/base/metadata/metadata_header_macros.h"
+#include "ui/base/metadata/metadata_impl_macros.h"
+#include "ui/events/event.h"
+#include "ui/events/keycodes/keyboard_codes.h"
+#include "ui/views/controls/button/button.h"
+#include "ui/views/controls/button/label_button.h"
+#include "ui/views/controls/webview/webview.h"
+#include "ui/views/focus/focus_manager.h"
+#include "ui/views/test/widget_test.h"
+#include "ui/views/view.h"
+#include "ui/views/widget/widget.h"
+
+namespace views {
+namespace {
+
+class TestButtonThatSkipsDefaultKeyEventProcessing : public LabelButton {
+  METADATA_HEADER(TestButtonThatSkipsDefaultKeyEventProcessing, LabelButton)
+
+ public:
+  TestButtonThatSkipsDefaultKeyEventProcessing()
+      : LabelButton(Button::PressedCallback(), u"Cancel") {}
+  ~TestButtonThatSkipsDefaultKeyEventProcessing() override = default;
+
+  bool SkipDefaultKeyEventProcessing(const ui::KeyEvent& event) override {
+    return true;
+  }
+};
+
+BEGIN_METADATA(TestButtonThatSkipsDefaultKeyEventProcessing)
+END_METADATA
+
+}  // namespace
+
+class UnhandledKeyboardEventHandlerTest : public views::test::WidgetTest {
+ public:
+  UnhandledKeyboardEventHandlerTest()
+      : views::test::WidgetTest(std::unique_ptr<base::test::TaskEnvironment>(
+            std::make_unique<content::BrowserTaskEnvironment>())) {}
+
+  UnhandledKeyboardEventHandlerTest(const UnhandledKeyboardEventHandlerTest&) =
+      delete;
+  UnhandledKeyboardEventHandlerTest& operator=(
+      const UnhandledKeyboardEventHandlerTest&) = delete;
+
+  ~UnhandledKeyboardEventHandlerTest() override = default;
+
+  std::unique_ptr<content::WebContents> CreateWebContentsForWebView(
+      content::BrowserContext* browser_context) {
+    return content::WebContentsTester::CreateTestWebContents(browser_context,
+                                                             nullptr);
+  }
+
+  void SetUp() override {
+    SetBrowserClientForTesting(&test_browser_client_);
+    rvh_enabler_ = std::make_unique<content::RenderViewHostTestEnabler>();
+    WebView::WebContentsCreator creator = base::BindRepeating(
+        &UnhandledKeyboardEventHandlerTest::CreateWebContentsForWebView,
+        base::Unretained(this));
+    scoped_web_contents_creator_ =
+        std::make_unique<WebView::ScopedWebContentsCreatorForTesting>(creator);
+    browser_context_ = std::make_unique<content::TestBrowserContext>();
+    WidgetTest::SetUp();
+
+    top_level_widget_ = CreateTopLevelFramelessPlatformWidget();
+    top_level_widget_->SetBounds(gfx::Rect(0, 10, 100, 100));
+    View* const contents_view =
+        top_level_widget_->SetContentsView(std::make_unique<View>());
+    web_view_ = contents_view->AddChildView(
+        std::make_unique<WebView>(browser_context_.get()));
+    top_level_widget_->Show();
+
+    focus_manager_ = top_level_widget_->GetFocusManager();
+    ASSERT_TRUE(focus_manager_);
+
+    focus_manager_->RegisterAccelerator(
+        ui::Accelerator(ui::VKEY_RETURN, ui::EF_NONE),
+        ui::AcceleratorManager::kNormalPriority, &target_);
+  }
+
+  void TearDown() override {
+    if (focus_manager_) {
+      focus_manager_->UnregisterAccelerators(&target_);
+      focus_manager_ = nullptr;
+    }
+    web_view_ = nullptr;
+    top_level_widget_.ExtractAsDangling()->CloseNow();
+    WidgetTest::TearDown();
+    browser_context_.reset();
+    scoped_web_contents_creator_.reset();
+    rvh_enabler_.reset();
+  }
+
+  input::NativeWebKeyboardEvent CreateReturnKeyDownEvent() {
+    input::NativeWebKeyboardEvent event(
+        blink::WebInputEvent::Type::kRawKeyDown,
+        blink::WebInputEvent::kNoModifiers,
+        blink::WebInputEvent::GetStaticTimeStampForTests());
+    event.windows_key_code = ui::VKEY_RETURN;
+    return event;
+  }
+
+ protected:
+  content::TestContentBrowserClient test_browser_client_;
+  std::unique_ptr<content::RenderViewHostTestEnabler> rvh_enabler_;
+  std::unique_ptr<content::TestBrowserContext> browser_context_;
+  std::unique_ptr<WebView::ScopedWebContentsCreatorForTesting>
+      scoped_web_contents_creator_;
+  raw_ptr<Widget> top_level_widget_ = nullptr;
+  raw_ptr<WebView> web_view_ = nullptr;
+  raw_ptr<FocusManager> focus_manager_ = nullptr;
+  ui::TestAcceleratorTarget target_;
+  UnhandledKeyboardEventHandler handler_;
+};
+
+// Verifies that renderer-unhandled Enter events do not fire accelerators when
+// focus is on a button that consumes key events (e.g., Cancel button).
+TEST_F(UnhandledKeyboardEventHandlerTest,
+       ReturnSkipsAcceleratorWhenButtonFocused) {
+  auto* button = top_level_widget_->GetContentsView()->AddChildView(
+      std::make_unique<TestButtonThatSkipsDefaultKeyEventProcessing>());
+  focus_manager_->SetFocusedView(button);
+  EXPECT_EQ(button, focus_manager_->GetFocusedView());
+
+  EXPECT_TRUE(button->SkipDefaultKeyEventProcessing(
+      ui::KeyEvent(ui::EventType::kKeyPressed, ui::VKEY_RETURN, ui::EF_NONE)));
+
+  EXPECT_TRUE(
+      handler_.HandleKeyboardEvent(CreateReturnKeyDownEvent(), focus_manager_));
+  EXPECT_EQ(0, target_.accelerator_count());
+}
+
+// Verifies that renderer-unhandled Enter events fire accelerators when a
+// WebView is focused, since its renderer already had a chance to consume the
+// key.
+TEST_F(UnhandledKeyboardEventHandlerTest,
+       ReturnFiresAcceleratorWhenWebViewFocused) {
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Bypass of tab-modal dialog protections via unhandled keyboard event redispatch

Flapjack, 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 flaw in how unhandled keyboard events are processed allows websites to bypass focus protections on tab-modal dialogs like File System Access prompts. By timing an ‘Enter’ keypress and returning it unconsumed, an attacker can trigger the dialog’s default ‘Allow’ action even when the ‘Cancel’ button is focused. Furthermore, these dialogs disable input event activation protection for key events by default, defeating rapid-interaction mitigations.

Affected files:

  • ui/views/controls/webview/unhandled_keyboard_event_handler.cc
  • chrome/browser/ui/file_system_access/file_system_access_permission_dialog.cc
  • ui/base/models/dialog_model.h

Estimated timestamp from git blame: Unknown (Google3 checkout)

Technical Details

A potential security vulnerability exists in how unhandled keyboard events are redispatched by the UnhandledKeyboardEventHandler on Windows, Linux, and ChromeOS. When a keyboard event (such as an ‘Enter’ keypress) is sent to a renderer and is not consumed, the browser process receives an INPUT_EVENT_ACK_STATE_NOT_CONSUMED notification. This unhandled event is routed to views::UnhandledKeyboardEventHandler::HandleKeyboardEvent.

For kRawKeyDown events, the handler extracts the associated accelerator (e.g., VKEY_RETURN) and directly calls focus_manager->ProcessAccelerator(accelerator). This direct invocation introduces a significant security bypass:

  1. Focus Protection Bypass: Standard keyboard event processing via FocusManager::OnKeyEvent includes a critical safety check: focused_view_->SkipDefaultKeyEventProcessing(event). This check is used to protect sensitive dialogs by setting the initial focus to a ‘Cancel’ button, which consumes the ‘Enter’ key and prevents it from acting as a global accelerator. By calling ProcessAccelerator directly, UnhandledKeyboardEventHandler skips this check. The event is instead processed by the AcceleratorManager, which finds and triggers the most recently registered handler for VKEY_RETURN: the dialog’s default ‘Allow’ button.

  2. Input Protection Bypass: Furthermore, dialogs built using ui::DialogModel (including the File System Access permission prompt) have enable_input_protection_ set to false by default (ui/base/models/dialog_model.h). Because of this, DialogClientView::ButtonPressed evaluates ShouldAllowKeyEventsDuringInputProtection() as true. This instructs the InputEventActivationProtector to explicitly ignore key events, allowing an ‘Enter’ keypress to be accepted immediately after the dialog appears, bypassing protections against rapid or accidental interactions.

Potential Attack Scenario

Note: These are suggested steps; our tooling agent does not yet have the ability to run code to confirm a working proof of concept.

  1. An attacker hosts a malicious website and persuades a user to press ‘Enter’ while interacting with the page (e.g., during a game).
  2. The website’s keydown event listener triggers and initiates a request for a sensitive permission, such as fileHandle.requestPermission({mode: 'readwrite'}).
  3. The script briefly blocks the renderer’s main thread (e.g., using a synchronous busy-wait loop). This ensures the browser process has enough time to initialize the tab-modal permission dialog and register its default ‘Allow’ button’s VKEY_RETURN accelerator.
  4. The script exits the event listener without calling preventDefault(), leaving the ‘Enter’ event unhandled.
  5. The browser redispatches the unhandled event. Due to the direct call to ProcessAccelerator, the dialog’s focused ‘Cancel’ button is bypassed, and the default ‘Allow’ button accelerator is triggered.
  6. Because input protection is disabled for key events, the action is immediately accepted, granting the attacker the requested capability without genuine user consent.
  1. Modify UnhandledKeyboardEventHandler::HandleKeyboardEvent to ensure that unhandled events are either re-injected through the standard FocusManager::OnKeyEvent flow, or explicitly check focused_view_->SkipDefaultKeyEventProcessing(event) before processing them as global accelerators.
  2. Security-sensitive dialogs (or ui::DialogModel by default) should explicitly enable enable_input_protection_ to mitigate rapid-interaction attacks via keyboard events.

Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff


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