Chrome · HTML
CVE-2026-113036
Memory Corruption in HTML
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
StyleScopeDatathird_party/blink/renderer/core/dom/element.h |
modified | |
TextVisitorthird_party/blink/renderer/core/dom/element.h |
modified | |
TrustedHTMLParserOptionsthird_party/blink/renderer/core/dom/element.h |
modified | |
UIEventthird_party/blink/renderer/core/dom/element.h |
modified | |
V8UnionBooleanOrScrollIntoViewOptionsthird_party/blink/renderer/core/dom/element.h |
modified | |
V8UnionCSSPseudoElementOrDocumentOrElementOrTextthird_party/blink/renderer/core/dom/element.h |
modified | |
V8UnionKeyframeAnimationOptionsOrUnrestrictedDoublethird_party/blink/renderer/core/dom/element.h |
modified | |
ListAttributeTargetObserverthird_party/blink/renderer/core/html/forms/html_input_element.h |
modified | |
RadioButtonGroupScopethird_party/blink/renderer/core/html/forms/html_input_element.h |
modified | |
ScriptObjectthird_party/blink/renderer/core/html/forms/html_input_element.h |
modified | |
UIEventthird_party/blink/renderer/core/html/forms/html_input_element.h |
modified | |
CORE_EXPORTthird_party/blink/renderer/core/html/forms/html_input_element.h |
modified | |
V8UnionHTMLOptGroupElementOrHTMLOptionElementthird_party/blink/renderer/core/html/forms/html_select_element.h |
modified | |
HTMLSelectedContentElementthird_party/blink/renderer/core/html/forms/html_select_element.h |
modified | |
SelectMutationObserverthird_party/blink/renderer/core/html/forms/html_select_element.h |
modified | |
UIEventthird_party/blink/renderer/core/html/forms/html_select_element.h |
modified | |
UIEventthird_party/blink/renderer/core/html/html_details_element.h |
modified | |
HTMLDetailsElementthird_party/blink/renderer/core/html/html_details_element.h |
modified | |
Documentthird_party/blink/renderer/core/html/html_dialog_element.h |
modified | |
ExceptionStatethird_party/blink/renderer/core/html/html_dialog_element.h |
modified | |
PointerEventthird_party/blink/renderer/core/html/html_dialog_element.h |
modified | |
UIEventthird_party/blink/renderer/core/html/html_dialog_element.h |
modified | |
KeyboardEventthird_party/blink/renderer/core/html/html_element.h |
modified | |
PointerEventthird_party/blink/renderer/core/html/html_element.h |
modified | |
TextControlElementthird_party/blink/renderer/core/html/html_element.h |
modified | |
UIEventthird_party/blink/renderer/core/html/html_element.h |
modified | |
V8UnionStringLegacyNullToEmptyStringOrTrustedScriptthird_party/blink/renderer/core/html/html_element.h |
modified | |
V8UnionBooleanOrTogglePopoverOptionsthird_party/blink/renderer/core/html/html_element.h |
modified | |
ShowPopoverOptionsthird_party/blink/renderer/core/html/html_element.h |
modified | |
ifthird_party/blink/renderer/core/html/html_menu_item_element.cc |
modified |
Files Changed
third_party/blink/renderer/core/html/media/html_media_element.hthird_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.htmlthird_party/blink/renderer/core/html/html_menu_item_element.hthird_party/blink/renderer/core/dom/element.hthird_party/blink/renderer/core/html/html_element.hthird_party/blink/renderer/core/html/html_details_element.hthird_party/blink/renderer/core/html/forms/html_form_control_element.ccthird_party/blink/renderer/core/html/html_menu_list_element.ccthird_party/blink/renderer/core/html/forms/html_select_element.ccthird_party/blink/renderer/core/html/media/html_media_element.ccthird_party/blink/renderer/core/html/html_element.ccthird_party/blink/renderer/core/html/forms/html_select_element.hthird_party/blink/renderer/core/html/forms/html_input_element.ccthird_party/blink/renderer/core/html/html_details_element.ccthird_party/blink/renderer/core/html/html_menu_list_element.hthird_party/blink/renderer/core/html/html_dialog_element.ccthird_party/blink/renderer/core/html/forms/html_input_element.hthird_party/blink/renderer/core/html/html_menu_item_element.ccthird_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/focus-without-user-activation.htmlthird_party/blink/renderer/core/html/forms/html_button_element.ccthird_party/blink/renderer/core/html/html_dialog_element.h
Patch
From ba1db4b3065871bdabd2822b6980df4ea122cac6 Mon Sep 17 00:00:00 2001 From: David Baron <[email protected]> Date: Mon, 21 Sep 2026 11:34:33 -0700 Subject: [PATCH] Don't use FocusTrigger::kUserGesture when handling untrusted key events. This makes menu elements stop using EventHandler::IsHandlingKeyEvent() and instead make decisions based on the event we're actually handling. Fixed: 553335083 Change-Id: Ie574a5595c9d476387ef3bcfa77a7244b00c83c0 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8427363 Reviewed-by: Joey Arhar <[email protected]> Commit-Queue: David Baron <[email protected]> Cr-Commit-Position: refs/heads/main@{#1701919} --- diff --git a/third_party/blink/renderer/core/dom/element.h b/third_party/blink/renderer/core/dom/element.h index eaa37b0..5c5e8e77 100644 --- a/third_party/blink/renderer/core/dom/element.h +++ b/third_party/blink/renderer/core/dom/element.h @@ -174,6 +174,7 @@ class StyleScopeData; class TextVisitor; class TrustedHTMLParserOptions; +class UIEvent; class V8UnionBooleanOrScrollIntoViewOptions; class V8UnionCSSPseudoElementOrDocumentOrElementOrText; class V8UnionKeyframeAnimationOptionsOrUnrestrictedDouble; @@ -1400,7 +1401,8 @@ return false; } virtual bool HandleCommandInternal(HTMLElement& invoker, - CommandEventType command) { + CommandEventType command, + UIEvent* activate_event) { CHECK(command != CommandEventType::kCustom && command != CommandEventType::kNone); diff --git a/third_party/blink/renderer/core/html/forms/html_button_element.cc b/third_party/blink/renderer/core/html/forms/html_button_element.cc index 243c838..4511cc9 100644 --- a/third_party/blink/renderer/core/html/forms/html_button_element.cc +++ b/third_party/blink/renderer/core/html/forms/html_button_element.cc @@ -35,6 +35,7 @@ #include "third_party/blink/renderer/core/dom/focus_params.h" #include "third_party/blink/renderer/core/dom/qualified_name.h" #include "third_party/blink/renderer/core/events/command_event.h" +#include "third_party/blink/renderer/core/events/ui_event.h" #include "third_party/blink/renderer/core/execution_context/execution_context.h" #include "third_party/blink/renderer/core/frame/web_feature.h" #include "third_party/blink/renderer/core/html/forms/form_data.h" @@ -261,7 +262,7 @@ } } - if (HandleCommandForActivation()) { + if (HandleCommandForActivation(To<UIEvent>(&event))) { return; } } diff --git a/third_party/blink/renderer/core/html/forms/html_form_control_element.cc b/third_party/blink/renderer/core/html/forms/html_form_control_element.cc index 45302f6..46bdaa6 100644 --- a/third_party/blink/renderer/core/html/forms/html_form_control_element.cc +++ b/third_party/blink/renderer/core/html/forms/html_form_control_element.cc @@ -38,6 +38,7 @@ #include "third_party/blink/renderer/core/event_type_names.h" #include "third_party/blink/renderer/core/events/command_event.h" #include "third_party/blink/renderer/core/events/keyboard_event.h" +#include "third_party/blink/renderer/core/events/ui_event.h" #include "third_party/blink/renderer/core/execution_context/execution_context.h" #include "third_party/blink/renderer/core/frame/web_feature.h" #include "third_party/blink/renderer/core/html/forms/html_button_element.h" @@ -484,7 +485,8 @@ } CHECK(popover.popover->IsValidBuiltinCommand(*this, action)); - popover.popover->HandleCommandInternal(*this, action); + popover.popover->HandleCommandInternal(*this, action, + To<UIEvent>(&event)); } } } diff --git a/third_party/blink/renderer/core/html/forms/html_input_element.cc b/third_party/blink/renderer/core/html/forms/html_input_element.cc index 76d2a460..fc27aad 100644 --- a/third_party/blink/renderer/core/html/forms/html_input_element.cc +++ b/third_party/blink/renderer/core/html/forms/html_input_element.cc @@ -2629,10 +2629,11 @@ } bool HTMLInputElement::HandleCommandInternal(HTMLElement& invoker, - CommandEventType command) { + CommandEventType command, + UIEvent* activate_event) { CHECK(IsValidBuiltinCommand(invoker, command)); - if (HTMLElement::HandleCommandInternal(invoker, command)) { + if (HTMLElement::HandleCommandInternal(invoker, command, activate_event)) { return true; } diff --git a/third_party/blink/renderer/core/html/forms/html_input_element.h b/third_party/blink/renderer/core/html/forms/html_input_element.h index 408e755c..10cb096 100644 --- a/third_party/blink/renderer/core/html/forms/html_input_element.h +++ b/third_party/blink/renderer/core/html/forms/html_input_element.h @@ -53,6 +53,7 @@ class ListAttributeTargetObserver; class RadioButtonGroupScope; class ScriptObject; +class UIEvent; struct DateTimeChooserParameters; class CORE_EXPORT HTMLInputElement @@ -426,7 +427,8 @@ bool IsValidBuiltinCommand(HTMLElement& invoker, CommandEventType command) override; bool HandleCommandInternal(HTMLElement& invoker, - CommandEventType command) override; + CommandEventType command, + UIEvent* activate_event) override; void SetFocused(bool is_focused, mojom::blink::FocusType, diff --git a/third_party/blink/renderer/core/html/forms/html_select_element.cc b/third_party/blink/renderer/core/html/forms/html_select_element.cc index a8c8c231..ec67bfe 100644 --- a/third_party/blink/renderer/core/html/forms/html_select_element.cc +++ b/third_party/blink/renderer/core/html/forms/html_select_element.cc @@ -1860,10 +1860,11 @@ } bool HTMLSelectElement::HandleCommandInternal(HTMLElement& invoker, - CommandEventType command) { + CommandEventType command, + UIEvent* activate_event) { CHECK(IsValidBuiltinCommand(invoker, command)); - if (HTMLElement::HandleCommandInternal(invoker, command)) { + if (HTMLElement::HandleCommandInternal(invoker, command, activate_event)) { return true; } diff --git a/third_party/blink/renderer/core/html/forms/html_select_element.h b/third_party/blink/renderer/core/html/forms/html_select_element.h index 7826c9ea..86b48598 100644 --- a/third_party/blink/renderer/core/html/forms/html_select_element.h +++ b/third_party/blink/renderer/core/html/forms/html_select_element.h @@ -54,6 +54,7 @@ class V8UnionHTMLOptGroupElementOrHTMLOptionElement; class HTMLSelectedContentElement; class SelectMutationObserver; +class UIEvent; enum class SelectPopupHideBehavior { kNormal, @@ -295,7 +296,8 @@ bool IsValidBuiltinCommand(HTMLElement& invoker, CommandEventType command) override; bool HandleCommandInternal(HTMLElement& invoker, - CommandEventType command) override; + CommandEventType command, + UIEvent* activate_event) override; // SlottedButton returns the first child <button> in the light dom tree. If // this select is in a state where the <button> can't be rendered, such as a diff --git a/third_party/blink/renderer/core/html/html_details_element.cc b/third_party/blink/renderer/core/html/html_details_element.cc index 2ad521f9..edc0262 100644 --- a/third_party/blink/renderer/core/html/html_details_element.cc +++ b/third_party/blink/renderer/core/html/html_details_element.cc @@ -305,11 +305,12 @@ } bool HTMLDetailsElement::HandleCommandInternal(HTMLElement& invoker, - CommandEventType command) { + CommandEventType command, + UIEvent* activate_event) { if (!IsValidBuiltinCommand(invoker, command)) { return false; } - if (HTMLElement::HandleCommandInternal(invoker, command)) { + if (HTMLElement::HandleCommandInternal(invoker, command, activate_event)) { return true; } diff --git a/third_party/blink/renderer/core/html/html_details_element.h b/third_party/blink/renderer/core/html/html_details_element.h index fee7abb1..51c389b2 100644 --- a/third_party/blink/renderer/core/html/html_details_element.h +++ b/third_party/blink/renderer/core/html/html_details_element.h @@ -27,6 +27,8 @@ namespace blink { +class UIEvent; + class HTMLDetailsElement final : public HTMLElement { DEFINE_WRAPPERTYPEINFO(); @@ -49,7 +51,8 @@ bool IsValidBuiltinCommand(HTMLElement& invoker, CommandEventType command) override; bool HandleCommandInternal(HTMLElement& invoker, - CommandEventType command) override; + CommandEventType command, + UIEvent* activate_event) override; // The name attribute for grouping of related details; empty string // means no grouping. diff --git a/third_party/blink/renderer/core/html/html_dialog_element.cc b/third_party/blink/renderer/core/html/html_dialog_element.cc index 1f0d86e..5718379 100644 --- a/third_party/blink/renderer/core/html/html_dialog_element.cc +++ b/third_party/blink/renderer/core/html/html_dialog_element.cc @@ -383,11 +383,12 @@ } bool HTMLDialogElement::HandleCommandInternal(HTMLElement& invoker, - CommandEventType command) { + CommandEventType command, + UIEvent* activate_event) { if (!IsValidBuiltinCommand(invoker, command)) { return false; } - if (HTMLElement::HandleCommandInternal(invoker, command)) { + if (HTMLElement::HandleCommandInternal(invoker, command, activate_event)) { return true; } diff --git a/third_party/blink/renderer/core/html/html_dialog_element.h b/third_party/blink/renderer/core/html/html_dialog_element.h index 7ad67e4..38d56bf 100644 --- a/third_party/blink/renderer/core/html/html_dialog_element.h +++ b/third_party/blink/renderer/core/html/html_dialog_element.h @@ -39,6 +39,7 @@ class Document; class ExceptionState; class PointerEvent; +class UIEvent; enum class ClosedByState { kAny, @@ -130,7 +131,8 @@ bool IsValidBuiltinCommand(HTMLElement& invoker, CommandEventType command) override; bool HandleCommandInternal(HTMLElement& invoker, - CommandEventType command) override; + CommandEventType command, + UIEvent* activate_event) override; void AttributeChanged(const AttributeModificationParams&) override; void ParseAttribute(const AttributeModificationParams&) override; diff --git a/third_party/blink/renderer/core/html/html_element.cc b/third_party/blink/renderer/core/html/html_element.cc index 86599e7..4e92bed 100644 --- a/third_party/blink/renderer/core/html/html_element.cc +++ b/third_party/blink/renderer/core/html/html_element.cc @@ -89,6 +89,7 @@ #include "third_party/blink/renderer/core/events/keyboard_event.h" #include "third_party/blink/renderer/core/events/pointer_event.h" #include "third_party/blink/renderer/core/events/toggle_event.h" +#include "third_party/blink/renderer/core/events/ui_event.h" #include "third_party/blink/renderer/core/frame/csp/content_security_policy.h" #include "third_party/blink/renderer/core/frame/local_dom_window.h" #include "third_party/blink/renderer/core/frame/local_frame.h" @@ -3254,11 +3255,12 @@ } bool HTMLElement::HandleCommandInternal(HTMLElement& invoker, - CommandEventType command) { + CommandEventType command, + UIEvent* activate_event) { if (!IsValidBuiltinCommand(invoker, command)) { return false; } - if (Element::HandleCommandInternal(invoker, command)) { + if (Element::HandleCommandInternal(invoker, command, activate_event)) { return true; } @@ -3389,7 +3391,7 @@ return false; } -bool HTMLElement::HandleCommandForActivation() { +bool HTMLElement::HandleCommandForActivation(UIEvent* activate_event) { if (!CanBeCommandInvoker()) { return false; } @@ -3429,7 +3431,8 @@ command_target->DispatchEvent(*command_event); if (!command_event->defaultPrevented() && command_event_type != CommandEventType::kCustom) { - command_target->HandleCommandInternal(*this, command_event_type); + command_target->HandleCommandInternal(*this, command_event_type, + activate_event); } return true; } @@ -4205,7 +4208,7 @@ return; } - if (HandleCommandForActivation()) { + if (HandleCommandForActivation(To<UIEvent>(&event))) { return; } } diff --git a/third_party/blink/renderer/core/html/html_element.h b/third_party/blink/renderer/core/html/html_element.h index cd12035..0a1a7ee 100644 --- a/third_party/blink/renderer/core/html/html_element.h +++ b/third_party/blink/renderer/core/html/html_element.h @@ -49,6 +49,7 @@ class KeyboardEvent; class PointerEvent; class TextControlElement; +class UIEvent; class V8UnionStringLegacyNullToEmptyStringOrTrustedScript; class V8UnionBooleanOrTogglePopoverOptions; class ShowPopoverOptions; @@ -392,7 +393,8 @@ CommandEventType command) override; bool IsValidBuiltinPopoverCommand(CommandEventType command); bool HandleCommandInternal(HTMLElement& invoker, - CommandEventType command) override; + CommandEventType command, + UIEvent* activate_event) override; // This is true if this element *can* be a command invoker: it is an element // type that supports command invokers (e.g. buttons and menuitems), and the // element isn't in a state that disqualifies it (e.g. a disabled state). This @@ -401,7 +403,7 @@ virtual bool CanBeCommandInvoker() const; static CommandEventType GetCommandEventType(const AtomicString& type, ExecutionContext*); - virtual bool HandleCommandForActivation(); + virtual bool HandleCommandForActivation(UIEvent* activate_event); Element* commandForElement() const; AtomicString command() const; void setCommand(const AtomicString& type); diff --git a/third_party/blink/renderer/core/html/html_menu_item_element.cc b/third_party/blink/renderer/core/html/html_menu_item_element.cc index ed65bcc8..625cde4 100644 --- a/third_party/blink/renderer/core/html/html_menu_item_element.cc +++ b/third_party/blink/renderer/core/html/html_menu_item_element.cc @@ -15,6 +15,7 @@ #include "third_party/blink/renderer/core/events/command_event.h" #include "third_party/blink/renderer/core/events/keyboard_event.h" #include "third_party/blink/renderer/core/events/mouse_event.h" +#include "third_party/blink/renderer/core/events/ui_event.h" #include "third_party/blink/renderer/core/execution_context/execution_context.h" #include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/frame/web_feature.h" @@ -26,7 +27,6 @@ #include "third_party/blink/renderer/core/html/html_sub_menu_element.h" #include "third_party/blink/renderer/core/html_names.h" #include "third_party/blink/renderer/core/input/event_handler.h" -#include "third_party/blink/renderer/core/input/keyboard_event_manager.h" #include "third_party/blink/renderer/core/keywords.h" #include "third_party/blink/renderer/platform/instrumentation/use_counter.h" #include "third_party/blink/renderer/platform/runtime_enabled_features.h" @@ -231,7 +231,8 @@ } } -void HTMLMenuItemElement::ActivateMenuItem() { +void HTMLMenuItemElement::ActivateMenuItem( + ActivationKeyboardEventType activation_type) { // A menu item's checkability and ability to invoke a command are // exclusive. If the item is checkable, that takes precedence, and the sub- // menu invoker will NOT be respected. @@ -248,13 +249,13 @@ submenu->InvokePopover(*this, PopoverInvokedVia::kCommand); - bool handling_keyboard_event = false; - if (LocalFrame* frame = GetDocument().GetFrame()) { - handling_keyboard_event = frame->GetEventHandler().IsHandlingKeyEvent(); - } if (submenu->popoverOpen()) { - if (handling_keyboard_event) { - submenu->FocusFirstItem(); + if (activation_type != ActivationKeyboardEventType::kNotKeyboard) { + FocusTrigger trigger = + activation_type == ActivationKeyboardEventType::kTrustedKeyboard + ? FocusTrigger::kUserGesture + : FocusTrigger::kScript; + submenu->FocusFirstItem(trigger); } else { // If this is resulting from a click, this is happening *before* that // click has had a chance to focus this menuitem. However, we should @@ -311,7 +312,10 @@ return; } - FocusParams focus_params(FocusTrigger::kUserGesture); + CHECK(event.IsFullyTrusted()) + << "FocusTrigger::kUserGesture depends on trusted events"; + FocusTrigger key_focus_trigger = FocusTrigger::kUserGesture; + FocusParams focus_params(key_focus_trigger); const AtomicString key(keyboard_event->key()); // Nothing else below does anything if we're not inside an owner menu that has @@ -530,12 +534,12 @@ if (auto* invoked_menulist = GetInvokedSubmenu()) { invoked_menulist->InvokePopover(*this, PopoverInvokedVia::kCommand); if (key == keywords::kArrowDown) { - if (invoked_menulist->FocusFirstItem()) { + if (invoked_menulist->FocusFirstItem(key_focus_trigger)) { event.SetDefaultHandled(); return; } ... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/focus-without-user-activation.html b/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/focus-without-user-activation.html
new file mode 100644
index 0000000..3ff214a1
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/focus-without-user-activation.html
@@ -0,0 +1,178 @@
+<!doctype html>
+<title>focusing first item in menu respects focus-without-user-activation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<body>
+ <input type="text" id="top-input" />
+ <script>
+ const tabKey = "\uE004";
+ const enterKey = "\uE007";
+ const shiftKey = "\uE008";
+
+ const waitForMessage = (expectedAction) => {
+ return new Promise((resolve) => {
+ const handler = (e) => {
+ if (e.data && e.data.action === expectedAction) {
+ window.removeEventListener("message", handler);
+ resolve(e.data);
+ }
+ };
+ window.addEventListener("message", handler);
+ });
+ };
+
+ function createTestIframe(t, target) {
+ const iframe = document.createElement("iframe");
+ iframe.allow = "focus-without-user-activation 'none'";
+ iframe.src = `/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.html?target=${target}`;
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+ return iframe;
+ }
+
+ // Test 1: Script click during key event on button commandfor menulist
+ promise_test(async (t) => {
+ const topInput = document.querySelector("#top-input");
+ const iframe = createTestIframe(t, "button");
+ await waitForMessage("ready");
+
+ const trigger = iframe.contentDocument.getElementById("trigger");
+ trigger.focus();
+ assert_equals(iframe.contentDocument.activeElement, trigger, "Trigger should have initial focus");
+
+ const shiftCompletePromise = waitForMessage("shift-click-complete");
+
+ // Press Shift key on the trigger in iframe.
+ // The Shift key handler will park focus on topInput and call trigger.click().
+ await new test_driver.Actions()
+ .keyDown(shiftKey)
+ .keyUp(shiftKey)
+ .send();
+
+ const msg = await shiftCompletePromise;
+
+ assert_true(msg.isPopoverOpen, "Popover should have opened");
+ assert_equals(
+ document.activeElement,
+ topInput,
+ "Top-level document should not lose focus on script click during key event",
+ );
+ assert_not_equals(
+ msg.activeElementId,
+ "first-item",
+ "First item should not have gained focus without user activation",
+ );
+ }, "Script click during key event on button commandfor menulist does not bypass focus-without-user-activation");
+
+ // Test 2: Script click during key event on menuitem activating submenu
+ promise_test(async (t) => {
+ const topInput = document.querySelector("#top-input");
+ const iframe = createTestIframe(t, "menuitem");
+ await waitForMessage("ready");
+
+ const trigger = iframe.contentDocument.getElementById("trigger");
+ trigger.focus();
+ assert_equals(iframe.contentDocument.activeElement, trigger, "Trigger should have initial focus");
+
+ const shiftCompletePromise = waitForMessage("shift-click-complete");
+
+ // Press Shift key on the trigger in iframe.
+ // The Shift key handler will park focus on topInput and call trigger.click().
+ await new test_driver.Actions()
+ .keyDown(shiftKey)
+ .keyUp(shiftKey)
+ .send();
+
+ const msg = await shiftCompletePromise;
+
+ assert_true(msg.isPopoverOpen, "Submenu should have opened");
+ assert_equals(
+ document.activeElement,
+ topInput,
+ "Top-level document should not lose focus on script click during key event",
+ );
+ assert_not_equals(
+ msg.activeElementId,
+ "first-item",
+ "Submenu first item should not have gained focus without user activation",
+ );
+ }, "Script click during key event on menuitem activating submenu does not bypass focus-without-user-activation");
+
+ // Test 3: Trusted key event on button commandfor menulist
+ promise_test(async (t) => {
+ const topInput = document.querySelector("#top-input");
+ const iframe = createTestIframe(t, "button");
+ await waitForMessage("ready");
+
+ topInput.focus();
+ assert_equals(document.activeElement, topInput, "Top input should have focus");
+
+ const itemFocusedPromise = waitForMessage("item-focused");
+
+ // Press Tab to navigate focus into the iframe's trigger button
+ await new test_driver.Actions()
+ .keyDown(tabKey)
+ .keyUp(tabKey)
+ .send();
+
+ // Press Enter on the focused trigger button
+ await new test_driver.Actions()
+ .keyDown(enterKey)
+ .keyUp(enterKey)
+ .send();
+
+ const msg = await itemFocusedPromise;
+ assert_true(msg.isPopoverOpen, "Popover should have opened");
+ assert_equals(
+ msg.activeElementId,
+ "first-item",
+ "First item in menulist should receive focus from trusted key event",
+ );
+ assert_equals(
+ document.activeElement,
+ iframe,
+ "Iframe should have focus after trusted key event",
+ );
+ }, "Trusted key event on button commandfor menulist focuses first item");
+
+ // Test 4: Trusted key event on menuitem activating submenu
+ promise_test(async (t) => {
+ const topInput = document.querySelector("#top-input");
+ const iframe = createTestIframe(t, "menuitem");
+ await waitForMessage("ready");
+
+ topInput.focus();
+ assert_equals(document.activeElement, topInput, "Top input should have focus");
+
+ const itemFocusedPromise = waitForMessage("item-focused");
+
+ // Press Tab to navigate focus into the iframe's trigger menuitem
+ await new test_driver.Actions()
+ .keyDown(tabKey)
+ .keyUp(tabKey)
+ .send();
+
+ // Press Enter on the focused trigger menuitem
+ await new test_driver.Actions()
+ .keyDown(enterKey)
+ .keyUp(enterKey)
+ .send();
+
+ const msg = await itemFocusedPromise;
+ assert_true(msg.isPopoverOpen, "Submenu should have opened");
+ assert_equals(
+ msg.activeElementId,
+ "first-item",
+ "First item in submenu should receive focus from trusted key event",
+ );
+ assert_equals(
+ document.activeElement,
+ iframe,
+ "Iframe should have focus after trusted key event",
+ );
+ }, "Trusted key event on menuitem activating submenu focuses first item");
+ </script>
+</body>
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.html b/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.html
new file mode 100644
index 0000000..9e182362
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<style>
+ body { margin: 0; padding: 0; }
+</style>
+<body>
+<script>
+const params = new URLSearchParams(location.search);
+const target = params.get("target") || "button";
+
+let trigger;
+let menulist;
+let firstItem;
+
+if (target === "button") {
+ document.body.innerHTML = `
+ <button id="trigger" commandfor="menulist" command="show-popover">Trigger</button>
+ <menulist id="menulist">
+ <menuitem id="first-item">First Item</menuitem>
+ <menuitem id="second-item">Second Item</menuitem>
+ </menulist>
+ `;
+} else {
+ document.body.innerHTML = `
+ <menubar>
+ <submenu>
+ <menuitem id="trigger">Trigger</menuitem>
+ <menulist id="menulist">
+ <menuitem id="first-item">First Item</menuitem>
+ <menuitem id="second-item">Second Item</menuitem>
+ </menulist>
+ </submenu>
+ </menubar>
+ `;
+}
+
+trigger = document.getElementById("trigger");
+menulist = document.getElementById("menulist");
+firstItem = document.getElementById("first-item");
+
+firstItem.addEventListener("focus", () => {
+ window.parent.postMessage({
+ action: "item-focused",
+ activeElementId: document.activeElement ? document.activeElement.id : null,
+ isPopoverOpen: menulist.matches(":popover-open")
+ }, "*");
+});
+
+trigger.addEventListener("keydown", (e) => {
+ if (e.key === "Shift") {
+ const parentInput = window.parent.document.getElementById("top-input");
+ if (parentInput) {
+ parentInput.focus();
+ }
+ trigger.click();
+ window.parent.postMessage({
+ action: "shift-click-complete",
+ activeElementId: document.activeElement ? document.activeElement.id : null,
+ isPopoverOpen: menulist.matches(":popover-open")
+ }, "*");
+ }
+});
+
+window.addEventListener("load", () => {
+ window.parent.postMessage({ action: "ready" }, "*");
+});
+</script>
+</body>
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