Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Views
DescriptionMissing authorization in Views
ComponentViews
Bug ClassAuthorization 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.
Tracker523277481
Fix commit0bdba256831a (chromium/src) +283/-9
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-12 (per vendor advisory)
Fix landed2026-08-05 (public git)
Disclosed2026-09-08
Patch gap34 days — fix public in source before the advisory

Background

`UnhandledKeyboardEventHandler`
a Views helper that takes keyboard events the renderer did not consume and re-dispatches them to the window’s FocusManager as potential accelerators.
`FocusManager::ProcessAccelerator()`
dispatches an accelerator (e.g. Enter) to the registered handler, which for a dialog is typically the default button’s action.
`SkipDefaultKeyEventProcessing()`
a per-View hook that lets the currently focused view (such as a focused Cancel button) claim a key for itself instead of letting it become an accelerator.
`enable_input_protection_`
a ui::DialogModel flag that, when set, ignores key/click input for a brief interval after a dialog appears to defeat rapid or accidental keystrokes; it defaults to false.

Root Cause Analysis

When web contents did not consume a key, UnhandledKeyboardEventHandler::HandleKeyboardEvent forwarded the resulting ui::Accelerator straight to FocusManager::ProcessAccelerator(). Unlike the normal FocusManager::OnKeyEvent() path, this route never consulted the focused view’s SkipDefaultKeyEventProcessing(), so the invariant “a focused control gets first refusal on its own keys” was violated: a focused non-default button (e.g. Cancel) could not claim Enter before the dialog’s default button accelerator fired. Combined with ui::DialogModel-built dialogs defaulting to enable_input_protection_ = false, an unconsumed Enter arriving from a web page could immediately fire the default action of a tab-modal prompt such as the File System Access permission dialog, effectively bypassing the focused Cancel.

The fix adds a should_skip_accelerator check (via the new FocusManager::ShouldSkipAcceleratorProcessing()) in both the kRawKeyDown and kKeyUp branches so that when a non-WebView view is focused and wants the key, the accelerator is suppressed, and it enables input protection on FileSystemAccessPermissionDialog as defense in depth.

Key insight
The redispatch-of-unhandled-keys path skipped the focused-view authorization check that the standard key path performs, letting a web-originated Enter reach a dialog’s default (Allow) accelerator even when a Cancel button was focused; the fix factors that check into ShouldSkipAcceleratorProcessing() and applies it on the redispatch path too.

Attack Path

  1. Present a sensitive tab-modal prompt A page triggers a File System Access request, causing FileSystemAccessPermissionDialog to appear with Cancel initially focused and (pre-patch) no input protection.
  2. Emit an unconsumed Enter The page produces an Enter key event that the renderer does not consume, so it bubbles up to UnhandledKeyboardEventHandler::HandleKeyboardEvent.
  3. Bypass the focused-view check Because that path called FocusManager::ProcessAccelerator() without checking SkipDefaultKeyEventProcessing(), the focused Cancel button never claims the Enter, and the accelerator is dispatched.
  4. Fire the default action The Enter accelerator resolves to the default Allow button, granting the permission (or confirming the prompt) without deliberate user consent. (inferred) that this yields silent File System Access grant, beyond what the diff alone proves.

Impact Assessment

An attacker-controlled page could bypass a focused Cancel button and trigger the default (Allow) action of a sensitive tab-modal prompt, most notably the File System Access permission dialog, effectively obtaining a permission grant the user did not intend. This runs in the browser UI/permission context and requires the prompt to be showing and an unconsumed Enter to reach it (e.g. via focus timing or rapid keystrokes). The advisory classifies this as a missing-authorization issue in Views; any broader escalation (persistent file access, data exfiltration) is (inferred) and not demonstrated by the patch, and no exploited-in-the-wild status is stated.

Changed Functions

FunctionChangeNotes
if
ui/views/controls/webview/web_dialog_view.cc
modified
TEST_F
chrome/browser/ui/file_system_access/file_system_access_permission_dialog_unittest.cc
modified
TestButtonThatSkipsDefaultKeyEventProcessing
ui/views/controls/webview/unhandled_keyboard_event_handler_unittest.cc
modified
UnhandledKeyboardEventHandlerTest
ui/views/controls/webview/unhandled_keyboard_event_handler_unittest.cc
modified
if
ui/views/controls/webview/unhandled_keyboard_event_handler_unittest.cc
modified

Files Changed

  • ui/base/test/test_dialog_model_host.cc
  • chrome/browser/ui/file_system_access/file_system_access_permission_dialog_unittest.cc
  • chrome/browser/ui/views/web_dialog_view_interactive_uitest.cc
  • ui/base/test/test_dialog_model_host.h
  • ui/views/controls/webview/unhandled_keyboard_event_handler_unittest.cc
  • ui/views/focus/focus_manager.cc
  • chrome/test/BUILD.gn
  • ui/views/focus/focus_manager.h
  • chrome/browser/ui/file_system_access/file_system_access_permission_dialog.cc
  • ui/views/controls/webview/web_dialog_view.cc
  • ui/views/controls/webview/unhandled_keyboard_event_handler.cc

Audit Directions

  • Divergent key-dispatch paths
    Audit every route that turns unconsumed web/renderer key events into ui::Accelerators (e.g. ProcessAccelerator() callers outside OnKeyEvent()) and confirm each honors SkipDefaultKeyEventProcessing() and priority handlers before firing.
  • Dialog input-protection defaults
    Review sensitive/security-relevant dialogs built via ui::DialogModel, which default enable_input_protection_ to false, and verify permission and confirmation prompts explicitly call SetEnableInputProtection(true).
  • Default-button vs. focused-control precedence
    Look for prompts where a “safe” control (Cancel) is initially focused but Enter can still activate a “dangerous” default (Allow), especially where key events originate outside the dialog’s own view tree.
From 0bdba256831acfd7e96b8cce9746becfcd0deae5 Mon Sep 17 00:00:00 2001
From: Mingyu Lei <leimy@chromium.org>
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 <leimy@chromium.org>
Reviewed-by: Dana Fried <dfried@chromium.org>
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/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;
 
+  auto should_skip_accelerator =
+      [focus_manager](const ui::Accelerator& accelerator) {
+        View* focused_view = focus_manager->GetFocusedView();
+        return focused_view && !IsViewClass<WebView>(focused_view) &&
+               focus_manager->ShouldSkipAcceleratorProcessing(accelerator);
+      };
+
   if (event.GetType() == blink::WebInputEvent::Type::kRawKeyDown) {
     ui::Accelerator accelerator =
         ui::GetAcceleratorFromNativeWebKeyboardEvent(event);
 
+    if (should_skip_accelerator(accelerator)) {
+      ignore_next_char_event_ = true;
+      return true;
+    }
+
     // This is tricky: we want to set ignore_next_char_event_ if
     // ProcessAccelerator returns true. But ProcessAccelerator might delete
     // |this| if the accelerator is a "close tab" one. So we speculatively
@@ -53,6 +67,11 @@
   if (event.GetType() == blink::WebInputEvent::Type::kKeyUp) {
     const ui::Accelerator accelerator =
         ui::GetAcceleratorFromNativeWebKeyboardEvent(event);
+
+    if (should_skip_accelerator(accelerator)) {
+      return true;
+    }
+
     if (focus_manager->ProcessAccelerator(accelerator)) {
       return true;
     }
diff --git a/ui/views/controls/webview/web_dialog_view.cc b/ui/views/controls/webview/web_dialog_view.cc
index 01eb0f3..985bb55 100644
--- a/ui/views/controls/webview/web_dialog_view.cc
+++ b/ui/views/controls/webview/web_dialog_view.cc
@@ -422,10 +422,6 @@
 bool WebDialogView::HandleKeyboardEvent(
     content::WebContents* source,
     const input::NativeWebKeyboardEvent& event) {
-  if (!event.os_event) {
-    return false;
-  }
-
   return unhandled_keyboard_event_handler_.HandleKeyboardEvent(
       event, GetFocusManager());
 }
diff --git a/ui/views/focus/focus_manager.cc b/ui/views/focus/focus_manager.cc
index 7bb61c0..4dd9876 100644
--- a/ui/views/focus/focus_manager.cc
+++ b/ui/views/focus/focus_manager.cc
@@ -48,6 +48,14 @@
                                  this);
 }
 
+bool FocusManager::ShouldSkipAcceleratorProcessing(
+    const ui::Accelerator& accelerator) const {
+  return focused_view_ &&
+         focused_view_->SkipDefaultKeyEventProcessing(
+             accelerator.ToKeyEvent()) &&
+         !accelerator_manager_.HasPriorityHandler(accelerator);
+}
+
 bool FocusManager::OnKeyEvent(const ui::KeyEvent& event) {
   const ui::KeyboardCode key_code = event.key_code();
 
@@ -63,8 +71,7 @@
   ui::Accelerator accelerator(event);
 
   // If the focused view wants to process the key event as is, let it be.
-  if (focused_view_ && focused_view_->SkipDefaultKeyEventProcessing(event) &&
-      !accelerator_manager_.HasPriorityHandler(accelerator)) {
+  if (ShouldSkipAcceleratorProcessing(accelerator)) {
     return true;
   }
 
diff --git a/ui/views/focus/focus_manager.h b/ui/views/focus/focus_manager.h
index f0afb13..2e7c86de 100644
--- a/ui/views/focus/focus_manager.h
+++ b/ui/views/focus/focus_manager.h
@@ -159,6 +159,11 @@
   // further.
   bool OnKeyEvent(const ui::KeyEvent& event);
 
+  // Returns true if the focused view wants to process the key event as is
+  // (and there is no priority handler registered for the accelerator).
+  bool ShouldSkipAcceleratorProcessing(
+      const ui::Accelerator& accelerator) const;
+
   // Returns true is the specified is part of the hierarchy of the window
   // associated with this FocusManager.
   bool ContainsView(View* view);
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/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() {
... (file diff truncated)
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 rj...@google.com

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