CVE-2026-13992
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/base/cocoa/command_dispatcher.mm |
modified | |
CommandDispatcherTestui/base/cocoa/command_dispatcher_unittest.mm |
modified | |
TEST_Fui/base/cocoa/command_dispatcher_unittest.mm |
modified |
Files Changed
ui/base/BUILD.gnui/base/cocoa/command_dispatcher.mmui/base/cocoa/command_dispatcher_unittest.mm
Patch
From eee6356c73bc7597e2be23640526b01f3f30d91b Mon Sep 17 00:00:00 2001 From: Bryan Oltman <[email protected]> Date: Wed, 20 May 2026 18:22:24 -0700 Subject: [PATCH] [macOS] Drop redispatched events if window is no longer key When an unhandled renderer event is returned for system processing, the CommandDispatcher redispatches it. If the window has lost its "key" status (e.g., due to a focus change while the event was in-flight), redispatching it can cause the event to be incorrectly handled by the newly focused window. This CL ensures that we only redispatch events if the target window is still the key window. Fixed: 514063409 Change-Id: I022d7f45d5c35d26a96b4efc55c0621eb73b3a80 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7864509 Reviewed-by: Avi Drissman <[email protected]> Commit-Queue: Bryan Oltman <[email protected]> Cr-Commit-Position: refs/heads/main@{#1633968} --- diff --git a/ui/base/BUILD.gn b/ui/base/BUILD.gn index 49a345c7..1add8ac9 100644 --- a/ui/base/BUILD.gn +++ b/ui/base/BUILD.gn @@ -1116,6 +1116,7 @@ "cocoa/base_view_unittest.mm", "cocoa/bubble_closer_unittest.mm", "cocoa/cocoa_base_utils_unittest.mm", + "cocoa/command_dispatcher_unittest.mm", "cocoa/constrained_window/constrained_window_animation_unittest.mm", "cocoa/defaults_utils_unittest.mm", "cocoa/nsmenu_additions_unittest.mm", diff --git a/ui/base/cocoa/command_dispatcher.mm b/ui/base/cocoa/command_dispatcher.mm index 5b05c78..f1b8c36 100644 --- a/ui/base/cocoa/command_dispatcher.mm +++ b/ui/base/cocoa/command_dispatcher.mm @@ -197,6 +197,12 @@ CHECK(eventType == NSEventTypeKeyDown || eventType == NSEventTypeKeyUp || eventType == NSEventTypeFlagsChanged); + // If the event's window is no longer the key window, don't attempt to + // redispatch it (https://crbug.com/514063409). + if (event.window && !event.window.keyWindow) { + return NO; + } + // Sometimes, an event will be redispatched from a child window to a parent // window to allow the parent window a chance to handle it. In that case, fix // up the native event to reference the correct window. Failure to do this can diff --git a/ui/base/cocoa/command_dispatcher_unittest.mm b/ui/base/cocoa/command_dispatcher_unittest.mm new file mode 100644 index 0000000..dc78fc4 --- /dev/null +++ b/ui/base/cocoa/command_dispatcher_unittest.mm @@ -0,0 +1,106 @@ +// 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/base/cocoa/command_dispatcher.h" + +#include "base/test/task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/gtest_mac.h" +#import "ui/base/test/cocoa_helper.h" + +@interface TestCommandDispatchingWindow + : CocoaTestHelperWindow <CommandDispatchingWindow> { + CommandDispatcher* __strong _dispatcher; +} +@end + +@implementation TestCommandDispatchingWindow + +- (instancetype)initWithContentRect:(NSRect)contentRect { + if ((self = [super initWithContentRect:contentRect])) { + _dispatcher = [[CommandDispatcher alloc] initWithOwner:self]; + } + return self; +} + +- (CommandDispatcher*)commandDispatcher { + return _dispatcher; +} + +- (NSWindow<CommandDispatchingWindow>*)commandDispatchParent { + return nil; +} + +- (void)setCommandHandler:(id<UserInterfaceItemCommandHandler>)commandHandler { +} + +- (BOOL)defaultPerformKeyEquivalent:(NSEvent*)event { + return NO; +} + +- (BOOL)defaultValidateUserInterfaceItem: + (id<NSValidatedUserInterfaceItem>)item { + return NO; +} + +- (void)commandDispatch:(id)sender { +} + +- (void)commandDispatchUsingKeyModifiers:(id)sender { +} + +@end + +namespace ui { + +class CommandDispatcherTest : public CocoaTest { + public: + void SetUp() override { + CocoaTest::SetUp(); + window_ = [[TestCommandDispatchingWindow alloc] init]; + key_event_ = [NSEvent keyEventWithType:NSEventTypeKeyDown + location:NSZeroPoint + modifierFlags:0 + timestamp:0 + windowNumber:window_.windowNumber + context:nil + characters:@"a" + charactersIgnoringModifiers:@"a" + isARepeat:NO + keyCode:0]; + } + + void TearDown() override { + key_event_ = nil; + [window_ close]; + window_ = nil; + CocoaTest::TearDown(); + } + + TestCommandDispatchingWindow* __strong window_; + NSEvent* __strong key_event_; + base::test::TaskEnvironment task_environment_{ + base::test::TaskEnvironment::MainThreadType::UI}; +}; + +// Verifies that -redispatchKeyEvent: drops redispatched events (unhandled +// renderer events returned for system processing) when the window is no longer +// key, as they would otherwise be incorrectly redirected to the now-key window. +TEST_F(CommandDispatcherTest, RedispatchDropsEventIfWindowNotKey) { + CommandDispatcher* dispatcher = [window_ commandDispatcher]; + window_.pretendIsKeyWindow = NO; + EXPECT_FALSE(window_.isKeyWindow); + EXPECT_FALSE([dispatcher redispatchKeyEvent:key_event_]); +} + +// Tests that -redispatchKeyEvent: correctly redispatches events when the +// window is still key, allowing normal system handling of unhandled keys. +TEST_F(CommandDispatcherTest, RedispatchSendsEventIfWindowIsKey) { + CommandDispatcher* dispatcher = [window_ commandDispatcher]; + window_.pretendIsKeyWindow = YES; + EXPECT_TRUE(window_.isKeyWindow); + EXPECT_TRUE([dispatcher redispatchKeyEvent:key_event_]); +} + +} // namespace ui
Regression Test / PoC
diff --git a/ui/base/cocoa/command_dispatcher_unittest.mm b/ui/base/cocoa/command_dispatcher_unittest.mm
new file mode 100644
index 0000000..dc78fc4
--- /dev/null
+++ b/ui/base/cocoa/command_dispatcher_unittest.mm
@@ -0,0 +1,106 @@
+// 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/base/cocoa/command_dispatcher.h"
+
+#include "base/test/task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/gtest_mac.h"
+#import "ui/base/test/cocoa_helper.h"
+
+@interface TestCommandDispatchingWindow
+ : CocoaTestHelperWindow <CommandDispatchingWindow> {
+ CommandDispatcher* __strong _dispatcher;
+}
+@end
+
+@implementation TestCommandDispatchingWindow
+
+- (instancetype)initWithContentRect:(NSRect)contentRect {
+ if ((self = [super initWithContentRect:contentRect])) {
+ _dispatcher = [[CommandDispatcher alloc] initWithOwner:self];
+ }
+ return self;
+}
+
+- (CommandDispatcher*)commandDispatcher {
+ return _dispatcher;
+}
+
+- (NSWindow<CommandDispatchingWindow>*)commandDispatchParent {
+ return nil;
+}
+
+- (void)setCommandHandler:(id<UserInterfaceItemCommandHandler>)commandHandler {
+}
+
+- (BOOL)defaultPerformKeyEquivalent:(NSEvent*)event {
+ return NO;
+}
+
+- (BOOL)defaultValidateUserInterfaceItem:
+ (id<NSValidatedUserInterfaceItem>)item {
+ return NO;
+}
+
+- (void)commandDispatch:(id)sender {
+}
+
+- (void)commandDispatchUsingKeyModifiers:(id)sender {
+}
+
+@end
+
+namespace ui {
+
+class CommandDispatcherTest : public CocoaTest {
+ public:
+ void SetUp() override {
+ CocoaTest::SetUp();
+ window_ = [[TestCommandDispatchingWindow alloc] init];
+ key_event_ = [NSEvent keyEventWithType:NSEventTypeKeyDown
+ location:NSZeroPoint
+ modifierFlags:0
+ timestamp:0
+ windowNumber:window_.windowNumber
+ context:nil
+ characters:@"a"
+ charactersIgnoringModifiers:@"a"
+ isARepeat:NO
+ keyCode:0];
+ }
+
+ void TearDown() override {
+ key_event_ = nil;
+ [window_ close];
+ window_ = nil;
+ CocoaTest::TearDown();
+ }
+
+ TestCommandDispatchingWindow* __strong window_;
+ NSEvent* __strong key_event_;
+ base::test::TaskEnvironment task_environment_{
+ base::test::TaskEnvironment::MainThreadType::UI};
+};
+
+// Verifies that -redispatchKeyEvent: drops redispatched events (unhandled
+// renderer events returned for system processing) when the window is no longer
+// key, as they would otherwise be incorrectly redirected to the now-key window.
+TEST_F(CommandDispatcherTest, RedispatchDropsEventIfWindowNotKey) {
+ CommandDispatcher* dispatcher = [window_ commandDispatcher];
+ window_.pretendIsKeyWindow = NO;
+ EXPECT_FALSE(window_.isKeyWindow);
+ EXPECT_FALSE([dispatcher redispatchKeyEvent:key_event_]);
+}
+
+// Tests that -redispatchKeyEvent: correctly redispatches events when the
+// window is still key, allowing normal system handling of unhandled keys.
+TEST_F(CommandDispatcherTest, RedispatchSendsEventIfWindowIsKey) {
+ CommandDispatcher* dispatcher = [window_ commandDispatcher];
+ window_.pretendIsKeyWindow = YES;
+ EXPECT_TRUE(window_.isKeyWindow);
+ EXPECT_TRUE([dispatcher redispatchKeyEvent:key_event_]);
+}
+
+} // namespace ui
Original Bug Report
Potential auto-confirmation of native macOS dialogs via keyboard event redispatch
Project Fortify, 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 the macOS keyboard event redispatching mechanism allows a malicious website to potentially auto-confirm native system dialogs. By delaying input acknowledgment, an attacker can ensure a redispatched ‘Enter’ key is delivered to a native panel after it gains focus. This bypasses user consent requirements for sensitive actions like file system access.
Affected files:
ui/base/cocoa/command_dispatcher.mmchrome/browser/ui/views/frame/browser_native_widget_mac.mmcontent/browser/renderer_host/render_widget_host_impl.cccomponents/remote_cocoa/app_shim/native_widget_mac_nswindow.mm
Estimated timestamp from git blame: Unknown (Google3 checkout)
Summary
There is a potential logic flaw in how keyboard events are redispatched on macOS when they are left unhandled by a renderer. An attacker could exploit this to automatically confirm native system dialogs (e.g., NSSavePanel or NSOpenPanel) without explicit user intent. This specifically impacts security-sensitive APIs that rely on native dialogs for user consent, such as the File System Access API.
Technical Details
On macOS, Chromium uses a redispatch mechanism to allow the system to perform default actions for keyboard events that the renderer chooses not to handle. This logic resides in ui/base/cocoa/command_dispatcher.mm. When an input acknowledgment (ack) is received indicating the event was unhandled, [CommandDispatcher redispatchKeyEvent:] is called, which re-injects the original NSEvent via [NSApp sendEvent:event].
According to macOS AppKit behavior, [NSApplication sendEvent:] redirects KeyDown events to the current keyWindow if the event’s associated window is not focused. When a native dialog like NSSavePanel is opened (e.g., via window.showSaveFilePicker()), it immediately becomes the keyWindow for the application.
If the renderer delays the input ack (for example, by performing a synchronous busy loop in JavaScript immediately after calling the picker API), the browser will only attempt to redispatch the ‘Enter’ key after the native dialog is already visible and focused. AppKit will then deliver the ‘Enter’ event directly to the dialog, which interprets it as a user confirmation (clicking the default “Save” or “Open” button).
Existing protections in Chrome, such as the isEventBeingRedispatched: check, are only implemented in Chrome’s custom NativeWidgetMacNSWindow subclasses. Native system panels do not inherit from these classes and therefore do not benefit from these safeguards. Consequently, they treat the redispatched event as fresh user input.
Potential Attack Steps
- A user interacts with a malicious site and presses the ‘Enter’ key.
- The site’s
keydownlistener triggers a native dialog, such aswindow.showSaveFilePicker(). - The JavaScript execution continues and performs a synchronous delay (e.g., a short
whileloop) before completing the event handler. - During this delay, the macOS native file picker appears and becomes the active
keyWindow. - Once the handler finishes and the ‘unhandled’ status is acked to the browser, the browser redispatches the ‘Enter’ key.
NSApplicationdelivers the redispatched ‘Enter’ to the focused native dialog.- The dialog confirms automatically, granting the site access to the file system at the suggested location.
Suggested Fix
The browser should verify that the window associated with the CommandDispatcher is still the keyWindow before proceeding with redispatchKeyEvent:. If the focus has shifted to a native panel or another window, the redispatch should be aborted to prevent accidental or malicious auto-confirmation. Additionally, implementing a short input lockout (similar to InputEventActivationProtector) for native macOS dialogs would provide a robust defense against such timing-based attacks.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
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.