Chrome · DOM
CVE-2026-87631
Logic Error in DOM
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/core/page/focusgroup_controller_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/page/focusgroup_controller.ccthird_party/blink/renderer/core/page/focusgroup_controller_test.cc
Patch
From 78f4142f505e1aeca80ce1bff1dfb1f3737d122a Mon Sep 17 00:00:00 2001 From: Jacques Newman <[email protected]> Date: Wed, 29 Jul 2026 21:07:04 -0700 Subject: [PATCH] [focusgroup] Ignore untrusted keyboard navigation FocusgroupController accepted script-created keydown events and moved focus with a forward or backward focus type, marking the target as focused from a user gesture. Reject untrusted keyboard events before focusgroup navigation. Add coverage for both navigation directions while preserving trusted keyboard behavior. Fixed: 537476242 Bug: 40210717 Change-Id: Ie75fc020926ecb8f143dd00534a685a3951e222b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8162543 Commit-Queue: Jacques Newman <[email protected]> Reviewed-by: Mason Freed <[email protected]> Cr-Commit-Position: refs/heads/main@{#1670818} --- diff --git a/third_party/blink/renderer/core/page/focusgroup_controller.cc b/third_party/blink/renderer/core/page/focusgroup_controller.cc index e9a3d8e..c7fadff 100644 --- a/third_party/blink/renderer/core/page/focusgroup_controller.cc +++ b/third_party/blink/renderer/core/page/focusgroup_controller.cc @@ -29,6 +29,9 @@ const LocalFrame* frame) { CHECK(frame); CHECK(frame->DomWindow()); + if (!event->isTrusted()) { + return false; + } ExecutionContext* context = frame->DomWindow()->GetExecutionContext(); if (!RuntimeEnabledFeatures::FocusgroupEnabled(context)) { return false; diff --git a/third_party/blink/renderer/core/page/focusgroup_controller_test.cc b/third_party/blink/renderer/core/page/focusgroup_controller_test.cc index ccd2b630..7c40d86 100644 --- a/third_party/blink/renderer/core/page/focusgroup_controller_test.cc +++ b/third_party/blink/renderer/core/page/focusgroup_controller_test.cc @@ -8,6 +8,7 @@ #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/input/web_keyboard_event.h" +#include "third_party/blink/renderer/bindings/core/v8/v8_keyboard_event_init.h" #include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/core/dom/shadow_root.h" #include "third_party/blink/renderer/core/events/keyboard_event.h" @@ -16,6 +17,7 @@ #include "third_party/blink/renderer/core/html/html_dialog_element.h" #include "third_party/blink/renderer/core/html/html_element.h" #include "third_party/blink/renderer/core/input/event_handler.h" +#include "third_party/blink/renderer/core/keywords.h" #include "third_party/blink/renderer/core/page/focusgroup_controller_utils.h" #include "third_party/blink/renderer/core/page/grid_focusgroup_structure_info.h" #include "third_party/blink/renderer/core/testing/page_test_base.h" @@ -46,6 +48,16 @@ return event; } + KeyboardEvent* UntrustedKeyDownEvent(const String& key, Element* target) { + KeyboardEventInit* init = KeyboardEventInit::Create(); + init->setBubbles(true); + init->setKey(key); + auto* event = + MakeGarbageCollected<KeyboardEvent>(event_type_names::kKeydown, init); + event->SetTarget(target); + return event; + } + void SendEvent(KeyboardEvent* event) { if (event->target()) { event->target()->DispatchEvent(*event); @@ -1110,6 +1122,100 @@ ASSERT_EQ(GetDocument().FocusedElement(), item1); } +TEST_F(FocusgroupControllerTest, + UntrustedArrowDownDoesNotMoveFocusOrMarkUserGesture) { + GetDocument().body()->SetInnerHTMLWithoutTrustedTypes(R"HTML( + <div focusgroup="menu block"> + <div id=first tabindex=0>First</div> + <div id=spellcheck tabindex=0 contenteditable spellcheck=true> + Spellcheck target + </div> + </div> + )HTML"); + UpdateAllLifecyclePhasesForTest(); + + auto* first = GetElementById("first"); + auto* spellcheck = GetElementById("spellcheck"); + ASSERT_TRUE(first); + ASSERT_TRUE(spellcheck); + + first->Focus(); + ASSERT_EQ(GetDocument().FocusedElement(), first); + ASSERT_FALSE(spellcheck->WasLastFocusFromUserGesture()); + + auto* event = UntrustedKeyDownEvent(keywords::kArrowDown, first); + ASSERT_FALSE(event->isTrusted()); + EXPECT_FALSE(FocusgroupController::HandleKeyboardEvent( + event, GetDocument().GetFrame())); + EXPECT_EQ(GetDocument().FocusedElement(), first); + EXPECT_FALSE(spellcheck->WasLastFocusFromUserGesture()); +} + +TEST_F(FocusgroupControllerTest, + UntrustedArrowUpDoesNotMoveFocusOrMarkUserGesture) { + GetDocument().body()->SetInnerHTMLWithoutTrustedTypes(R"HTML( + <div focusgroup="menu block"> + <div id=spellcheck tabindex=0 contenteditable spellcheck=true> + Spellcheck target + </div> + <div id=last tabindex=0>Last</div> + </div> + )HTML"); + UpdateAllLifecyclePhasesForTest(); + + auto* spellcheck = GetElementById("spellcheck"); + auto* last = GetElementById("last"); + ASSERT_TRUE(spellcheck); + ASSERT_TRUE(last); + + last->Focus(); + ASSERT_EQ(GetDocument().FocusedElement(), last); + ASSERT_FALSE(spellcheck->WasLastFocusFromUserGesture()); + + auto* event = UntrustedKeyDownEvent(keywords::kArrowUp, last); + ASSERT_FALSE(event->isTrusted()); + EXPECT_FALSE(FocusgroupController::HandleKeyboardEvent( + event, GetDocument().GetFrame())); + EXPECT_EQ(GetDocument().FocusedElement(), last); + EXPECT_FALSE(spellcheck->WasLastFocusFromUserGesture()); +} + +TEST_F(FocusgroupControllerTest, TrustedArrowKeysMoveFocusAndMarkUserGesture) { + GetDocument().body()->SetInnerHTMLWithoutTrustedTypes(R"HTML( + <div focusgroup="menu block"> + <div id=first tabindex=0>First</div> + <div id=spellcheck tabindex=0 contenteditable spellcheck=true> + Spellcheck target + </div> + <div id=last tabindex=0>Last</div> + </div> + )HTML"); + UpdateAllLifecyclePhasesForTest(); + + auto* first = GetElementById("first"); + auto* spellcheck = GetElementById("spellcheck"); + auto* last = GetElementById("last"); + ASSERT_TRUE(first); + ASSERT_TRUE(spellcheck); + ASSERT_TRUE(last); + + first->Focus(); + ASSERT_EQ(GetDocument().FocusedElement(), first); + ASSERT_FALSE(spellcheck->WasLastFocusFromUserGesture()); + + SendArrowDown(first); + EXPECT_EQ(GetDocument().FocusedElement(), spellcheck); + EXPECT_TRUE(spellcheck->WasLastFocusFromUserGesture()); + + last->Focus(); + ASSERT_EQ(GetDocument().FocusedElement(), last); + ASSERT_FALSE(spellcheck->WasLastFocusFromUserGesture()); + + SendArrowUp(last); + EXPECT_EQ(GetDocument().FocusedElement(), spellcheck); + EXPECT_TRUE(spellcheck->WasLastFocusFromUserGesture()); +} + TEST_F(FocusgroupControllerTest, NestedFocusgroupsHaveSeparateScopes) { GetDocument().body()->SetHTMLUnsafeWithoutTrustedTypes(R"HTML( <div id=outer focusgroup="toolbar">
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/renderer/core/page/focusgroup_controller_test.cc b/third_party/blink/renderer/core/page/focusgroup_controller_test.cc
index ccd2b630..7c40d86 100644
--- a/third_party/blink/renderer/core/page/focusgroup_controller_test.cc
+++ b/third_party/blink/renderer/core/page/focusgroup_controller_test.cc
@@ -8,6 +8,7 @@
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/input/web_keyboard_event.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_keyboard_event_init.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/events/keyboard_event.h"
@@ -16,6 +17,7 @@
#include "third_party/blink/renderer/core/html/html_dialog_element.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/input/event_handler.h"
+#include "third_party/blink/renderer/core/keywords.h"
#include "third_party/blink/renderer/core/page/focusgroup_controller_utils.h"
#include "third_party/blink/renderer/core/page/grid_focusgroup_structure_info.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
@@ -46,6 +48,16 @@
return event;
}
+ KeyboardEvent* UntrustedKeyDownEvent(const String& key, Element* target) {
+ KeyboardEventInit* init = KeyboardEventInit::Create();
+ init->setBubbles(true);
+ init->setKey(key);
+ auto* event =
+ MakeGarbageCollected<KeyboardEvent>(event_type_names::kKeydown, init);
+ event->SetTarget(target);
+ return event;
+ }
+
void SendEvent(KeyboardEvent* event) {
if (event->target()) {
event->target()->DispatchEvent(*event);
@@ -1110,6 +1122,100 @@
ASSERT_EQ(GetDocument().FocusedElement(), item1);
}
+TEST_F(FocusgroupControllerTest,
+ UntrustedArrowDownDoesNotMoveFocusOrMarkUserGesture) {
+ GetDocument().body()->SetInnerHTMLWithoutTrustedTypes(R"HTML(
+ <div focusgroup="menu block">
+ <div id=first tabindex=0>First</div>
+ <div id=spellcheck tabindex=0 contenteditable spellcheck=true>
+ Spellcheck target
+ </div>
+ </div>
+ )HTML");
+ UpdateAllLifecyclePhasesForTest();
+
+ auto* first = GetElementById("first");
+ auto* spellcheck = GetElementById("spellcheck");
+ ASSERT_TRUE(first);
+ ASSERT_TRUE(spellcheck);
+
+ first->Focus();
+ ASSERT_EQ(GetDocument().FocusedElement(), first);
+ ASSERT_FALSE(spellcheck->WasLastFocusFromUserGesture());
+
+ auto* event = UntrustedKeyDownEvent(keywords::kArrowDown, first);
+ ASSERT_FALSE(event->isTrusted());
+ EXPECT_FALSE(FocusgroupController::HandleKeyboardEvent(
+ event, GetDocument().GetFrame()));
+ EXPECT_EQ(GetDocument().FocusedElement(), first);
+ EXPECT_FALSE(spellcheck->WasLastFocusFromUserGesture());
+}
+
+TEST_F(FocusgroupControllerTest,
+ UntrustedArrowUpDoesNotMoveFocusOrMarkUserGesture) {
+ GetDocument().body()->SetInnerHTMLWithoutTrustedTypes(R"HTML(
+ <div focusgroup="menu block">
+ <div id=spellcheck tabindex=0 contenteditable spellcheck=true>
+ Spellcheck target
+ </div>
+ <div id=last tabindex=0>Last</div>
+ </div>
+ )HTML");
+ UpdateAllLifecyclePhasesForTest();
+
+ auto* spellcheck = GetElementById("spellcheck");
+ auto* last = GetElementById("last");
+ ASSERT_TRUE(spellcheck);
+ ASSERT_TRUE(last);
+
+ last->Focus();
+ ASSERT_EQ(GetDocument().FocusedElement(), last);
+ ASSERT_FALSE(spellcheck->WasLastFocusFromUserGesture());
+
+ auto* event = UntrustedKeyDownEvent(keywords::kArrowUp, last);
+ ASSERT_FALSE(event->isTrusted());
+ EXPECT_FALSE(FocusgroupController::HandleKeyboardEvent(
+ event, GetDocument().GetFrame()));
+ EXPECT_EQ(GetDocument().FocusedElement(), last);
+ EXPECT_FALSE(spellcheck->WasLastFocusFromUserGesture());
+}
+
+TEST_F(FocusgroupControllerTest, TrustedArrowKeysMoveFocusAndMarkUserGesture) {
+ GetDocument().body()->SetInnerHTMLWithoutTrustedTypes(R"HTML(
+ <div focusgroup="menu block">
+ <div id=first tabindex=0>First</div>
+ <div id=spellcheck tabindex=0 contenteditable spellcheck=true>
+ Spellcheck target
+ </div>
+ <div id=last tabindex=0>Last</div>
+ </div>
+ )HTML");
+ UpdateAllLifecyclePhasesForTest();
+
+ auto* first = GetElementById("first");
+ auto* spellcheck = GetElementById("spellcheck");
+ auto* last = GetElementById("last");
+ ASSERT_TRUE(first);
+ ASSERT_TRUE(spellcheck);
+ ASSERT_TRUE(last);
+
+ first->Focus();
+ ASSERT_EQ(GetDocument().FocusedElement(), first);
+ ASSERT_FALSE(spellcheck->WasLastFocusFromUserGesture());
+
+ SendArrowDown(first);
+ EXPECT_EQ(GetDocument().FocusedElement(), spellcheck);
+ EXPECT_TRUE(spellcheck->WasLastFocusFromUserGesture());
+
+ last->Focus();
+ ASSERT_EQ(GetDocument().FocusedElement(), last);
+ ASSERT_FALSE(spellcheck->WasLastFocusFromUserGesture());
+
+ SendArrowUp(last);
+ EXPECT_EQ(GetDocument().FocusedElement(), spellcheck);
+ EXPECT_TRUE(spellcheck->WasLastFocusFromUserGesture());
+}
+
TEST_F(FocusgroupControllerTest, NestedFocusgroupsHaveSeparateScopes) {
GetDocument().body()->SetHTMLUnsafeWithoutTrustedTypes(R"HTML(
<div id=outer focusgroup="toolbar">
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