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 <janewman@microsoft.com>
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 <janewman@microsoft.com>
Reviewed-by: Mason Freed <masonf@chromium.org>
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