Low watch CVSS 6.5 chrome Authorization Error 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in DOM
DescriptionMissing authorization in DOM
ComponentDOM
Sub-componentFocusgroup
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.
Tracker537476242
Fix commit78f4142f505e (chromium/src) +109/-0
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-21 (per vendor advisory)
Fix landed2026-07-30 (public git)
Disclosed2026-09-08
Patch gap40 days — fix public in source before the advisory

Background

Focusgroup
a Blink DOM feature (the focusgroup attribute) that lets arrow keys move focus among a set of related focusable elements.
`FocusgroupController`
the Blink controller whose HandleKeyboardEvent interprets keydown events and performs forward/backward focus navigation within a focusgroup.
`Event::isTrusted()`
a flag that is true only for events the user agent itself dispatched, and false for events constructed and dispatched by script.
`WasLastFocusFromUserGesture()`
a per-element flag recording whether the element’s most recent focus was the result of a genuine user gesture, which downstream features consult as an authorization signal.

Root Cause Analysis

The changed function (FocusgroupController::HandleKeyboardEvent, whose signature (..., const LocalFrame* frame) with CHECK(frame) / CHECK(frame->DomWindow()) matches the function the added test calls directly — the @@ header does not name it) processed every keydown event that reached it without verifying the event was trusted. As the commit message states, the controller “accepted script-created keydown events and moved focus with a forward or backward focus type, marking the target as focused from a user gesture.” A page could therefore construct a synthetic KeyboardEvent (whose isTrusted() is false), dispatch it into a focusgroup, and cause FocusgroupController to both move focus and set the target’s WasLastFocusFromUserGesture() flag. The violated invariant is that focus navigation and user-gesture attribution must originate only from genuine user input, not from dispatchEvent.

The fix inserts if (!event->isTrusted()) { return false; } immediately after the two CHECKs and before any of the feature-enablement or navigation logic, so an untrusted event exits early with no focus change and no gesture marking. This tightens the check: it adds a guard that did not previously exist.

Key insight
The single mistake is that FocusgroupController::HandleKeyboardEvent trusted any keydown event, including script-forged ones, letting a page spoof a user-gesture-driven focus move; the fix rejects events for which isTrusted() is false before any navigation runs.

Attack Path

  1. Build a focusgroup The attacker page contains a focusgroup container holding focusable items, including a sensitive target such as a contenteditable/spellcheck element, with an element inside it focused.
  2. Forge a keydown Script constructs an untrusted KeyboardEvent of type keydown with key set to ArrowDown or ArrowUp, so isTrusted() returns false.
  3. Dispatch into the focusgroup Dispatching the event to the focused element routes it to FocusgroupController::HandleKeyboardEvent, which pre-patch processed it as if it were real user input.
  4. Spoof user-gesture focus Focus moves to the adjacent focusgroup item and that element’s WasLastFocusFromUserGesture() becomes true, forging a user-gesture attribution without any user interaction.
  5. Abuse the spoofed gesture (inferred) Behaviors gated on user-gesture-initiated focus can be reached without a genuine gesture; the patch demonstrates only the focus move and gesture-flag spoof, not any specific downstream escalation.

Impact Assessment

An attacker running script in a page can programmatically move focus within a focusgroup and mark the newly focused element as focused-from-a-user-gesture, entirely within the renderer/web-content context, with the only precondition that the page uses the focusgroup feature. This defeats an authorization signal meant to distinguish real user input from script, matching the reported class (Missing authorization in DOM). Any escalation beyond the focus move and user-gesture spoof the patch demonstrates is (inferred), and this analysis makes no exploited-in-the-wild or severity claim beyond what the patch shows.

Changed Functions

FunctionChangeNotes
TEST_F
third_party/blink/renderer/core/page/focusgroup_controller_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/page/focusgroup_controller_test.cc
  • third_party/blink/renderer/core/page/focusgroup_controller.cc

Audit Directions

  • Missing `isTrusted()` guards
    Review other event-driven controllers reachable via dispatchEvent (keyboard, pointer, focus handlers) that perform navigation or state changes, and confirm they reject events for which isTrusted() is false.
  • User-gesture attribution from events
    Audit any code path that sets WasLastFocusFromUserGesture() or user-activation state as a side effect of handling an event, ensuring the event’s trustedness is validated first.
  • Synthetic-event reachability
    Look for DOM features that assume incoming events are user-generated, and verify script cannot forge the same effect by constructing and dispatching a matching event.
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.