Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in CSS
DescriptionInformation leak in CSS
ComponentCSS
Bug ClassLogic Error
Tracker518023156
Fix commit2f467ce00a65 (chromium/src) +53/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
TEST_F
third_party/blink/renderer/core/html/forms/html_select_element_test.cc
modified
for
third_party/blink/renderer/core/html/forms/html_select_element_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/dom/scroll_marker_group_data.cc
  • third_party/blink/renderer/core/html/forms/html_select_element_test.cc
From 2f467ce00a657f3267bcb324ecd8b265d2470586 Mon Sep 17 00:00:00 2001
From: Daniil Sakhapov <sakhapov@chromium.org>
Date: Fri, 24 Jul 2026 03:34:26 -0700
Subject: [PATCH] Don't update selected scroll marker for previewed form controls

When a form control such as a <select size> listbox is showing an
autofill preview, it is scrolled to bring the suggested value into view
even though the suggestion has not yet been accepted.
ScrollMarkerGroupData::ChooseMarkerRecursively() reads the scroll offset
of any scroll container that holds scroll marker targets, including such
a control, and exposes the result through :target-current.

Match ScrollStateQuerySnapshot and Element::scrollTop() by keeping the
existing selected scroll marker while the control is in preview state.
This covers both anchor-based scroll-target-group markers that target
<option> elements and ::scroll-marker pseudo-elements on the form
control itself.

Add a unit test that exercises the anchor-based variant against a
<select size> listbox.

Fixed: 518023156
Change-Id: If936913c9a3abab6bdb0689e3a70edb001348042
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8137011
Reviewed-by: Rune Lillesveen <futhark@chromium.org>
Commit-Queue: Daniil Sakhapov <sakhapov@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1667762}
---

diff --git a/third_party/blink/renderer/core/dom/scroll_marker_group_data.cc b/third_party/blink/renderer/core/dom/scroll_marker_group_data.cc
index 478f1b0f..4aedfef5 100644
--- a/third_party/blink/renderer/core/dom/scroll_marker_group_data.cc
+++ b/third_party/blink/renderer/core/dom/scroll_marker_group_data.cc
@@ -8,6 +8,7 @@
 #include "third_party/blink/renderer/core/dom/element.h"
 #include "third_party/blink/renderer/core/dom/layout_tree_builder_traversal.h"
 #include "third_party/blink/renderer/core/dom/scroll_marker_group_pseudo_element.h"
+#include "third_party/blink/renderer/core/html/forms/html_form_control_element.h"
 #include "third_party/blink/renderer/core/html/html_anchor_element.h"
 #include "third_party/blink/renderer/core/layout/geometry/axis.h"
 #include "third_party/blink/renderer/core/layout/layout_box.h"
@@ -587,6 +588,13 @@
     if (targets.empty()) {
       break;
     }
+    // Form controls in autofill preview state may have been scrolled to bring
+    // the previewed value into view. Keep the current selection so that the
+    // suggested value cannot be observed via the selected scroll marker.
+    if (auto* form_control = DynamicTo<HTMLFormControlElement>(scroller);
+        form_control && form_control->IsPreviewed()) {
+      return selected_marker_;
+    }
     LayoutBox* scroller_box = scroller->GetLayoutBox();
     DCHECK(scroller_box);
     ScrollableArea* scrollable_area = scroller_box->GetScrollableArea();
diff --git a/third_party/blink/renderer/core/html/forms/html_select_element_test.cc b/third_party/blink/renderer/core/html/forms/html_select_element_test.cc
index 62825d5cc..73f7dcf 100644
--- a/third_party/blink/renderer/core/html/forms/html_select_element_test.cc
+++ b/third_party/blink/renderer/core/html/forms/html_select_element_test.cc
@@ -12,6 +12,7 @@
 #include "third_party/blink/renderer/core/css/resolver/style_resolver.h"
 #include "third_party/blink/renderer/core/css/style_engine.h"
 #include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/scroll_marker_group_data.h"
 #include "third_party/blink/renderer/core/dom/shadow_root.h"
 #include "third_party/blink/renderer/core/events/keyboard_event.h"
 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
@@ -36,6 +37,7 @@
 #include "third_party/blink/renderer/platform/keyboard_codes.h"
 #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
 #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
 
 namespace blink {
 
@@ -107,6 +109,49 @@
   EXPECT_EQ(select->UserHasEditedTheField(), true);
 }
 
+TEST_F(HTMLSelectElementTest, ListBoxSuggestedOptionScrollTargetGroup) {
+  StringBuilder html;
+  html.Append(
+      "<!DOCTYPE HTML>"
+      "<style>nav { scroll-target-group: auto }</style>"
+      "<select id='sel' size='4'>");
+  for (int i = 0; i < 20; ++i) {
+    html.AppendFormat("<option id='o%d' value='v%d'>o%d</option>", i, i, i);
+  }
+  html.Append("</select><nav id='nav'>");
+  for (int i = 0; i < 20; ++i) {
+    html.AppendFormat("<a id='a%d' href='#o%d'></a>", i, i);
+  }
+  html.Append("</nav>");
+  SetHtmlInnerHTML(html.ToString().Utf8());
+  test::RunPendingTasks();
+  UpdateAllLifecyclePhasesForTest();
+
+  auto* select = To<HTMLSelectElement>(GetElementById("sel"));
+  Element* nav = GetElementById("nav");
+  Element* first_anchor = GetElementById("a0");
+  ScrollMarkerGroupData* group = nav->GetScrollTargetGroupData();
+  ASSERT_TRUE(group);
+  ASSERT_EQ(group->Selected(), first_anchor);
+
+  // Setting the suggested option scrolls the listbox to bring it into view,
+  // but the selected scroll marker should not follow that scroll while the
+  // suggestion has not been accepted.
+  select->SetSuggestedValue("v15");
+  ASSERT_TRUE(select->IsPreviewed());
+  test::RunPendingTasks();
+  UpdateAllLifecyclePhasesForTest();
+  EXPECT_EQ(group->Selected(), first_anchor);
+
+  // Once the suggestion is cleared and a value is committed, the selected
+  // scroll marker tracks the listbox scroll position again.
+  select->setValueForBinding("v15");
+  ASSERT_FALSE(select->IsPreviewed());
+  test::RunPendingTasks();
+  UpdateAllLifecyclePhasesForTest();
+  EXPECT_NE(group->Selected(), first_anchor);
+}
+
 TEST_F(HTMLSelectElementTest, SaveRestoreSelectSingleFormControlState) {
   SetHtmlInnerHTML(
       "<!DOCTYPE HTML><select id='sel'>"
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/core/html/forms/html_select_element_test.cc b/third_party/blink/renderer/core/html/forms/html_select_element_test.cc
index 62825d5cc..73f7dcf 100644
--- a/third_party/blink/renderer/core/html/forms/html_select_element_test.cc
+++ b/third_party/blink/renderer/core/html/forms/html_select_element_test.cc
@@ -12,6 +12,7 @@
 #include "third_party/blink/renderer/core/css/resolver/style_resolver.h"
 #include "third_party/blink/renderer/core/css/style_engine.h"
 #include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/scroll_marker_group_data.h"
 #include "third_party/blink/renderer/core/dom/shadow_root.h"
 #include "third_party/blink/renderer/core/events/keyboard_event.h"
 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
@@ -36,6 +37,7 @@
 #include "third_party/blink/renderer/platform/keyboard_codes.h"
 #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
 #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
 
 namespace blink {
 
@@ -107,6 +109,49 @@
   EXPECT_EQ(select->UserHasEditedTheField(), true);
 }
 
+TEST_F(HTMLSelectElementTest, ListBoxSuggestedOptionScrollTargetGroup) {
+  StringBuilder html;
+  html.Append(
+      "<!DOCTYPE HTML>"
+      "<style>nav { scroll-target-group: auto }</style>"
+      "<select id='sel' size='4'>");
+  for (int i = 0; i < 20; ++i) {
+    html.AppendFormat("<option id='o%d' value='v%d'>o%d</option>", i, i, i);
+  }
+  html.Append("</select><nav id='nav'>");
+  for (int i = 0; i < 20; ++i) {
+    html.AppendFormat("<a id='a%d' href='#o%d'></a>", i, i);
+  }
+  html.Append("</nav>");
+  SetHtmlInnerHTML(html.ToString().Utf8());
+  test::RunPendingTasks();
+  UpdateAllLifecyclePhasesForTest();
+
+  auto* select = To<HTMLSelectElement>(GetElementById("sel"));
+  Element* nav = GetElementById("nav");
+  Element* first_anchor = GetElementById("a0");
+  ScrollMarkerGroupData* group = nav->GetScrollTargetGroupData();
+  ASSERT_TRUE(group);
+  ASSERT_EQ(group->Selected(), first_anchor);
+
+  // Setting the suggested option scrolls the listbox to bring it into view,
+  // but the selected scroll marker should not follow that scroll while the
+  // suggestion has not been accepted.
+  select->SetSuggestedValue("v15");
+  ASSERT_TRUE(select->IsPreviewed());
+  test::RunPendingTasks();
+  UpdateAllLifecyclePhasesForTest();
+  EXPECT_EQ(group->Selected(), first_anchor);
+
+  // Once the suggestion is cleared and a value is committed, the selected
+  // scroll marker tracks the listbox scroll position again.
+  select->setValueForBinding("v15");
+  ASSERT_FALSE(select->IsPreviewed());
+  test::RunPendingTasks();
+  UpdateAllLifecyclePhasesForTest();
+  EXPECT_NE(group->Selected(), first_anchor);
+}
+
 TEST_F(HTMLSelectElementTest, SaveRestoreSelectSingleFormControlState) {
   SetHtmlInnerHTML(
       "<!DOCTYPE HTML><select id='sel'>"
Loading diff…

Original Bug Report

reported by vm...@google.com

Autofill preview identity leak via CSS Scroll Markers

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 potential security bypass exists in Blink’s CSS Scroll Markers subsystem where an attacker-controlled page can observe which option in a <select> listbox a user is hovering over during an Autofill preview. Due to a missing IsPreviewed() check when resolving scroll offset for scroll marker snapshots, the unmasked preview scroll position can be leaked to the page through the :target-current pseudo-class.

Affected files:

  • third_party/blink/renderer/core/dom/scroll_marker_group_data.cc
  • third_party/blink/renderer/core/scroll/scrollable_area.cc

Estimated timestamp from git blame: 2025-03-11

Description

There is a potential information disclosure vulnerability in Blink’s CSS Scroll Markers API. It could allow a malicious website to determine which Autofill suggestion a user is currently hovering over or previewing in a <select> listbox before they accept or commit to it. This represents a potential bypass of the mitigations introduced for crbug.com/1261689.

When a user hovers over an Autofill suggestion, the browser triggers a real programmatic scroll of the <select> listbox element to bring the previewed <option> into view. While existing scroll-related APIs (such as scrollTop and ScrollStateQuerySnapshot::CanExposeScrollOffsets()) have been patched with IsPreviewed() checks to hide this programmatic scroll, the CSS Scroll Markers subsystem (ScrollMarkerGroupData) lacks equivalent validation.

An attacker could use anchor-based scroll markers (scroll-target-group) or pseudo-elements (scroll-marker-group) to observe the :target-current pseudo-class state. By checking the pseudo-class state (via CSS styling and JavaScript getComputedStyle), the attacker can identify exactly which <option> has been scrolled into view.

Root Cause Analysis

  1. Programmatic scroll during Autofill preview: In third_party/blink/renderer/core/html/forms/select_type.cc, the listbox scroll is initiated during the preview phase via ListBoxSelectType::DidSetSuggestedOption -> ScrollToOption:

    void ListBoxSelectType::DidSetSuggestedOption(HTMLOptionElement* option) {
      if (!select_->GetLayoutObject())
        return;
      ...
      ScrollToOption(option);
    }
    
  2. Unmasked Scroll Offset Read in CSS Scroll Markers: When a programmatic scroll occurs, PaintLayerScrollableArea::UpdateScrollMarkers() triggers UpdateSelectedScrollMarker() on ScrollMarkerGroupData, setting the invalidation state to kNeedsFullUpdate.

    During the next lifecycle update, ScrollMarkerGroupData::UpdateSnapshot() calls ChooseMarkerRecursively() which reads the unmasked preview scroll offset:

    active = ChooseMarker(scrollable_area->GetScrollOffsetForScrollMarkerUpdate(),
                          scrollable_area, scroller_box, targets);
    

    Neither scrollable_area->GetScrollOffsetForScrollMarkerUpdate() nor the caller checks whether the scroller is a form control element currently in the preview state (i.e., IsPreviewed()).

  3. State Exposure: ChooseMarker selects the corresponding scroll marker for the target <option> at the previewed scroll position, and ApplyPendingScrollMarker() updates the selected marker state, triggering PseudoStateChanged(kPseudoTargetCurrent) on the scroll marker element. The page can style :target-current and read its state using getComputedStyle in JS, revealing the hovered option.

Potential Impact

If confirmed, this vulnerability allows same-origin renderer-process disclosure of PII from Autofill suggestions (such as country, state, card type, or address line selectors) before the user has consented to filling the form.

Potential Steps to Reproduce

(Note: These are suggested/potential steps; our tooling agent does not yet have the ability to run code.)

  1. Render a scrollable listbox with multiple options and assign unique IDs to them:
    <select size="4" name="country" autocomplete="country">
      <option id="o0">US</option>
      <option id="o1">UK</option>
    </select>
    
  2. Set up scroll target groups pointing to those option IDs:
    <nav style="scroll-target-group: auto;">
      <a href="#o0">US</a>
      <a href="#o1">UK</a>
    </nav>
    
  3. Apply a custom style rule targeting :target-current:
    nav a:target-current { color: rgb(13, 37, 0); }
    
  4. Periodically query the computed color of the links using getComputedStyle(a).color via requestAnimationFrame.
  5. Trigger Autofill and hover suggestions in the popup, observing if the script detects the active preview options.

Suggested Fix

Mask the returned scroll offset during scroll marker snapshot updates if the target element is currently in the Autofill preview state. This can be fixed in ScrollableArea::GetScrollOffsetForScrollMarkerUpdate() inside third_party/blink/renderer/core/scroll/scrollable_area.cc:

ScrollOffset ScrollableArea::GetScrollOffsetForScrollMarkerUpdate() {
  if (LayoutBox* box = GetLayoutBox()) {
    if (auto* form_control = DynamicTo<HTMLFormControlElement>(box->GetNode())) {
      if (form_control->IsPreviewed()) {
        return ScrollOffset();
      }
    }
  }
  ...
}

Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040


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.

View on issue tracker