CVE-2026-79291
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/core/html/forms/html_select_element_test.cc |
modified | |
forthird_party/blink/renderer/core/html/forms/html_select_element_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/dom/scroll_marker_group_data.ccthird_party/blink/renderer/core/html/forms/html_select_element_test.cc
Patch
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'>"
Regression Test / PoC
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'>"
Original Bug Report
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.ccthird_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
-
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 viaListBoxSelectType::DidSetSuggestedOption->ScrollToOption:void ListBoxSelectType::DidSetSuggestedOption(HTMLOptionElement* option) { if (!select_->GetLayoutObject()) return; ... ScrollToOption(option); } -
Unmasked Scroll Offset Read in CSS Scroll Markers: When a programmatic scroll occurs,
PaintLayerScrollableArea::UpdateScrollMarkers()triggersUpdateSelectedScrollMarker()onScrollMarkerGroupData, setting the invalidation state tokNeedsFullUpdate.During the next lifecycle update,
ScrollMarkerGroupData::UpdateSnapshot()callsChooseMarkerRecursively()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()). -
State Exposure:
ChooseMarkerselects the corresponding scroll marker for the target<option>at the previewed scroll position, andApplyPendingScrollMarker()updates the selected marker state, triggeringPseudoStateChanged(kPseudoTargetCurrent)on the scroll marker element. The page can style:target-currentand read its state usinggetComputedStylein 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.)
- 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> - 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> - Apply a custom style rule targeting
:target-current:nav a:target-current { color: rgb(13, 37, 0); } - Periodically query the computed color of the links using
getComputedStyle(a).colorviarequestAnimationFrame. - 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.