Medium watch CVSS 6.5 chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in CSS
DescriptionInformation leak in CSS
ComponentCSS
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker518023156
Fix commit2f467ce00a65 (chromium/src) +53/-0
CWECWE-200 (Information exposure)
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-05-29 (per vendor advisory)
Fix landed2026-07-24 (public git)
Disclosed2026-08-25
Patch gap32 days — fix public in source before the advisory

Background

Scroll marker group
A CSS scroll-target/scroll-marker mechanism (ScrollMarkerGroupData) that tracks which target (e.g. an <a> anchor or ::scroll-marker) is “current” based on a scroll container’s scroll offset, exposing the result through the :target-current pseudo-class.
`ChooseMarkerRecursively()`
The ScrollMarkerGroupData routine that walks scroll containers holding scroll-marker targets and reads each container’s scroll offset to decide which marker is selected.
Autofill preview state
A transient state in which a form control such as a <select size> listbox displays a suggested (not-yet-accepted) value; entering it can auto-scroll the control to bring the suggestion into view (IsPreviewed()).
Observable-discrepancy leak
When internal state that should not be web-observable (here, the previewed autofill suggestion) becomes inferable through a visible/queryable side effect such as the selected scroll marker.

Root Cause Analysis

ScrollMarkerGroupData::ChooseMarkerRecursively() iterates over scroll containers that hold scroll-marker targets and reads each container’s scroll offset to compute the selected marker. When a <select size> listbox (or another form control) enters autofill preview state, Blink scrolls it to reveal the previewed option; the routine then read that preview-driven scroll offset and updated the selected scroll marker accordingly, exposing which option was previewed via :target-current — even though the suggestion had not been accepted and should be invisible to the page. This diverged from ScrollStateQuerySnapshot and Element::scrollTop(), which already suppress preview-induced scroll observation.

The fix adds an early check: if the current scroller is an HTMLFormControlElement and form_control->IsPreviewed() is true, the function returns the existing selected_marker_ unchanged, so the preview scroll never influences the marker. The check runs before scroller->GetLayoutBox() and the scroll-offset read, so the previewed offset is never consulted for that container.

Key insight
The scroll-marker selection logic trusted the raw scroll offset of any scroll container, including a form control auto-scrolled for an unaccepted autofill preview; the fix special-cases previewed form controls to freeze the existing selection so the suggested value cannot leak through :target-current.

Attack Path

  1. Build an observable scroll-marker mapping The page creates a <select size='4'> listbox with many <option>s and a scroll-target-group <nav> whose anchors target those options (or uses ::scroll-marker pseudo-elements on the control), so option position maps to a distinct selected marker.
  2. Induce an autofill preview A suggested value is surfaced for the listbox (SetSuggestedValue), putting it in IsPreviewed() state and auto-scrolling it to bring the suggested option into view.
  3. Read the leaked selection Because ChooseMarkerRecursively() read the preview-driven scroll offset, the selected scroll marker moves to the anchor corresponding to the suggested option, and the page observes it via :target-current styling.
  4. Recover the suggested value By mapping which marker became current, the page infers the previewed autofill suggestion before the user accepts it. (inferred: chaining this into recovery of specific autofill/credential contents depends on what values the preview reveals.)

Impact Assessment

A malicious web page can infer the value the browser is previewing in a form control (an autofill suggestion the user has not accepted) purely through CSS scroll-marker observation in the renderer, without any user interaction beyond the preview appearing. The advisory rates this an information leak / observable discrepancy in CSS with no claim of in-the-wild exploitation; any broader disclosure of sensitive autofill data beyond the previewed option is (inferred). The precondition is that the site controls the page markup (listbox plus scroll-target-group or ::scroll-marker) and a preview is triggered for that control.

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

Audit Directions

  • Preview/suggested state observability
    Audit every consumer of a form control’s scroll offset or geometry (scrollTop, scroll snapshots, intersection/anchor logic) to confirm it suppresses or freezes values while IsPreviewed() is true, matching ScrollStateQuerySnapshot and Element::scrollTop().
  • Scroll-offset-driven observable CSS state
    Review other features that derive web-observable state (:target-current, scroll-snap, container/scroll-state queries) from raw scroll offsets, since any internally-driven scroll (autofill, spellcheck, find-in-page) can leak into them.
  • Consistency across parallel readers of the same signal
    When one code path is hardened against previewed state, search for sibling code paths reading the same underlying signal that were not updated, as this fix restored parity that ChooseMarkerRecursively() had missed.
From 2f467ce00a657f3267bcb324ecd8b265d2470586 Mon Sep 17 00:00:00 2001
From: Daniil Sakhapov <[email protected]>
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 <[email protected]>
Commit-Queue: Daniil Sakhapov <[email protected]>
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 [email protected]

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