Chrome · Autofill
CVE-2026-87478
Logic Error in Autofill
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
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/element.ccthird_party/blink/renderer/core/dom/scroll_marker_group_data.ccthird_party/blink/renderer/core/html/forms/html_select_element_test.cc
Patch
From b12935e03a81ffbfcbb68a792dc9d51ab6b968ea Mon Sep 17 00:00:00 2001
From: Jochen Eisinger <jochen@chromium.org>
Date: Mon, 10 Aug 2026 03:52:21 -0700
Subject: [PATCH] [autofill] Implement unified popover preview for <select> controls
Standardize <select> form controls (both MenuListSelectType and
ListBoxSelectType) to render Autofill previews using a top-layer popover
overlay (autofill_popover_ and autofill_popover_text_).
In MenuListSelectType, remove the appearance:base-select condition so
that any suggested option triggers the popover overlay, and update
OptionToBeShown() to never return suggested options for standard
menulists.
In ListBoxSelectType, instantiate autofill_popover_ and
autofill_popover_text_ during shadow DOM initialization and implement
GetAutofillPreviewElement() to return the popover instance.
Update html.css to apply full-control anchor sizing, start text
alignment with inline padding, and enforce -webkit-small-control font
pinning on the popover overlay.
TAG=agy
CONV=4259a896-ab67-4bff-bd6c-afa15ec4ad96
Bug: 40060525, 40057398, 518032534, 540015493, 517219513, 540021850
Change-Id: Ic0e2d79fd518479afeb36bf32870f889d32591f1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8175874
Commit-Queue: Jochen Eisinger <jochen@chromium.org>
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Reviewed-by: Christoph Schwering <schwering@google.com>
Cr-Commit-Position: refs/heads/main@{#1676341}
---
diff --git a/third_party/blink/renderer/core/dom/element.cc b/third_party/blink/renderer/core/dom/element.cc
index 938dd8e7..cdfe1c4 100644
--- a/third_party/blink/renderer/core/dom/element.cc
+++ b/third_party/blink/renderer/core/dom/element.cc
@@ -2679,10 +2679,12 @@
}
// Don't disclose scroll position in preview state. See crbug.com/1261689.
- auto* select_element = DynamicTo<HTMLSelectElement>(this);
- if (select_element && !select_element->UsesMenuList() &&
- select_element->IsPreviewed()) {
- return 0;
+ if (!RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled()) {
+ auto* select_element = DynamicTo<HTMLSelectElement>(this);
+ if (select_element && !select_element->UsesMenuList() &&
+ select_element->IsPreviewed()) {
+ return 0;
+ }
}
LayoutBox* box = GetLayoutBoxForScrolling();
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 feea272..3678350 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
@@ -14,6 +14,7 @@
#include "third_party/blink/renderer/core/layout/layout_box.h"
#include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
#include "third_party/blink/renderer/core/scroll/scroll_into_view_util.h"
+#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/wtf_size_t.h"
namespace blink {
@@ -590,9 +591,11 @@
// 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_;
+ if (!RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled()) {
+ if (auto* form_control = DynamicTo<HTMLFormControlElement>(scroller);
+ form_control && form_control->IsPreviewed()) {
+ return selected_marker_;
+ }
}
LayoutBox* scroller_box = scroller->GetLayoutBox();
DCHECK(scroller_box);
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 b2c7f16..67dbac7 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
@@ -110,6 +110,36 @@
EXPECT_EQ(select->UserHasEditedTheField(), true);
}
+TEST_F(HTMLSelectElementTest, MenuListAutofillPreviewDisabledFallback) {
+ ScopedSelectAutofillPopoverPreviewForTest disable_popover_preview(false);
+ SetHtmlInnerHTML(
+ "<!DOCTYPE HTML><select id='sel'>"
+ "<option value='111' selected>111</option>"
+ "<option value='222'>222</option></select>");
+ auto* select = To<HTMLSelectElement>(GetElementById("sel"));
+
+ // MenuList always supports implicit anchor for the ::picker popover.
+ EXPECT_TRUE(select->MayBeImplicitAnchor());
+
+ // When SelectAutofillPopoverPreview is disabled, the shadow DOM popover
+ // preview element is omitted.
+ EXPECT_EQ(nullptr, select->GetAutofillPreviewElement());
+ EXPECT_EQ("111", select->InnerElement().textContent());
+
+ // Setting the suggested value mutates the menulist inner text node directly
+ // via OptionToBeShown().
+ select->SetSuggestedValue("222");
+ ASSERT_TRUE(select->IsPreviewed());
+ EXPECT_EQ("222", select->InnerElement().textContent());
+ EXPECT_EQ("111", select->SelectedOption()->value());
+ EXPECT_EQ(nullptr, select->GetAutofillPreviewElement());
+
+ // Clearing the preview restores the original selection's inner text.
+ select->SetSuggestedValue("");
+ ASSERT_FALSE(select->IsPreviewed());
+ EXPECT_EQ("111", select->InnerElement().textContent());
+}
+
TEST_F(HTMLSelectElementTest, ListBoxSuggestedOptionScrollTargetGroup) {
StringBuilder html;
html.Append(
@@ -135,9 +165,8 @@
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.
+ // Setting the suggested option displays a popover preview overlay without
+ // scrolling the listbox, so the selected scroll marker is unchanged.
select->SetSuggestedValue("v15");
ASSERT_TRUE(select->IsPreviewed());
test::RunPendingTasks();
@@ -145,7 +174,7 @@
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.
+ // option is scrolled into view and the scroll marker tracks it.
select->setValueForBinding("v15");
ASSERT_FALSE(select->IsPreviewed());
test::RunPendingTasks();
@@ -153,6 +182,98 @@
EXPECT_NE(group->Selected(), first_anchor);
}
+TEST_F(HTMLSelectElementTest,
+ ListBoxAutofillPreviewDoesNotScrollOrResetScroll) {
+ StringBuilder html;
+ html.Append("<!DOCTYPE HTML><select id='sel' size='4'>");
+ for (int i = 0; i < 20; ++i) {
+ html.AppendFormat("<option id='o%d' value='v%d'>option %d</option>", i, i,
+ i);
+ }
+ html.Append("</select>");
+ SetHtmlInnerHTML(html.ToString().Utf8());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+
+ auto* select = To<HTMLSelectElement>(GetElementById("sel"));
+
+ // When enabled, listbox supports implicit anchor for autofill popover.
+ EXPECT_TRUE(select->MayBeImplicitAnchor());
+
+ // 1. Initial preview does not scroll the listbox.
+ EXPECT_EQ(0.0, select->scrollTop());
+ select->SetSuggestedValue("v15");
+ ASSERT_TRUE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(0.0, select->scrollTop());
+
+ // 2. Clear preview.
+ select->SetSuggestedValue("");
+ ASSERT_FALSE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(0.0, select->scrollTop());
+
+ // 3. User scrolls the listbox.
+ select->setScrollTop(50);
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ double scrolled_top = select->scrollTop();
+ EXPECT_GT(scrolled_top, 0.0);
+
+ // 4. Setting a suggested value does not alter the listbox scroll position or
+ // mask scrollTop().
+ select->SetSuggestedValue("v15");
+ ASSERT_TRUE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(scrolled_top, select->scrollTop());
+
+ // 5. Clearing the suggested value preserves the user's scroll position.
+ select->SetSuggestedValue("");
+ ASSERT_FALSE(select->IsPreviewed());
+ test::RunPendingTasks();
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 b2c7f16..67dbac7 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
@@ -110,6 +110,36 @@
EXPECT_EQ(select->UserHasEditedTheField(), true);
}
+TEST_F(HTMLSelectElementTest, MenuListAutofillPreviewDisabledFallback) {
+ ScopedSelectAutofillPopoverPreviewForTest disable_popover_preview(false);
+ SetHtmlInnerHTML(
+ "<!DOCTYPE HTML><select id='sel'>"
+ "<option value='111' selected>111</option>"
+ "<option value='222'>222</option></select>");
+ auto* select = To<HTMLSelectElement>(GetElementById("sel"));
+
+ // MenuList always supports implicit anchor for the ::picker popover.
+ EXPECT_TRUE(select->MayBeImplicitAnchor());
+
+ // When SelectAutofillPopoverPreview is disabled, the shadow DOM popover
+ // preview element is omitted.
+ EXPECT_EQ(nullptr, select->GetAutofillPreviewElement());
+ EXPECT_EQ("111", select->InnerElement().textContent());
+
+ // Setting the suggested value mutates the menulist inner text node directly
+ // via OptionToBeShown().
+ select->SetSuggestedValue("222");
+ ASSERT_TRUE(select->IsPreviewed());
+ EXPECT_EQ("222", select->InnerElement().textContent());
+ EXPECT_EQ("111", select->SelectedOption()->value());
+ EXPECT_EQ(nullptr, select->GetAutofillPreviewElement());
+
+ // Clearing the preview restores the original selection's inner text.
+ select->SetSuggestedValue("");
+ ASSERT_FALSE(select->IsPreviewed());
+ EXPECT_EQ("111", select->InnerElement().textContent());
+}
+
TEST_F(HTMLSelectElementTest, ListBoxSuggestedOptionScrollTargetGroup) {
StringBuilder html;
html.Append(
@@ -135,9 +165,8 @@
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.
+ // Setting the suggested option displays a popover preview overlay without
+ // scrolling the listbox, so the selected scroll marker is unchanged.
select->SetSuggestedValue("v15");
ASSERT_TRUE(select->IsPreviewed());
test::RunPendingTasks();
@@ -145,7 +174,7 @@
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.
+ // option is scrolled into view and the scroll marker tracks it.
select->setValueForBinding("v15");
ASSERT_FALSE(select->IsPreviewed());
test::RunPendingTasks();
@@ -153,6 +182,98 @@
EXPECT_NE(group->Selected(), first_anchor);
}
+TEST_F(HTMLSelectElementTest,
+ ListBoxAutofillPreviewDoesNotScrollOrResetScroll) {
+ StringBuilder html;
+ html.Append("<!DOCTYPE HTML><select id='sel' size='4'>");
+ for (int i = 0; i < 20; ++i) {
+ html.AppendFormat("<option id='o%d' value='v%d'>option %d</option>", i, i,
+ i);
+ }
+ html.Append("</select>");
+ SetHtmlInnerHTML(html.ToString().Utf8());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+
+ auto* select = To<HTMLSelectElement>(GetElementById("sel"));
+
+ // When enabled, listbox supports implicit anchor for autofill popover.
+ EXPECT_TRUE(select->MayBeImplicitAnchor());
+
+ // 1. Initial preview does not scroll the listbox.
+ EXPECT_EQ(0.0, select->scrollTop());
+ select->SetSuggestedValue("v15");
+ ASSERT_TRUE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(0.0, select->scrollTop());
+
+ // 2. Clear preview.
+ select->SetSuggestedValue("");
+ ASSERT_FALSE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(0.0, select->scrollTop());
+
+ // 3. User scrolls the listbox.
+ select->setScrollTop(50);
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ double scrolled_top = select->scrollTop();
+ EXPECT_GT(scrolled_top, 0.0);
+
+ // 4. Setting a suggested value does not alter the listbox scroll position or
+ // mask scrollTop().
+ select->SetSuggestedValue("v15");
+ ASSERT_TRUE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(scrolled_top, select->scrollTop());
+
+ // 5. Clearing the suggested value preserves the user's scroll position.
+ select->SetSuggestedValue("");
+ ASSERT_FALSE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(scrolled_top, select->scrollTop());
+}
+
+TEST_F(HTMLSelectElementTest, ListBoxAutofillPreviewDisabledFallback) {
+ ScopedSelectAutofillPopoverPreviewForTest disable_popover_preview(false);
+ StringBuilder html;
+ html.Append("<!DOCTYPE HTML><select id='sel' size='4'>");
+ for (int i = 0; i < 20; ++i) {
+ html.AppendFormat("<option id='o%d' value='v%d'>option %d</option>", i, i,
+ i);
+ }
+ html.Append("</select>");
+ SetHtmlInnerHTML(html.ToString().Utf8());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+
+ auto* select = To<HTMLSelectElement>(GetElementById("sel"));
+
+ // Popover preview element is omitted when feature is disabled.
+ EXPECT_EQ(nullptr, select->GetAutofillPreviewElement());
+ EXPECT_EQ(0.0, select->scrollTop());
+
+ // Setting the suggested value scrolls the listbox to the previewed option,
+ // but scrollTop() is masked to 0.0 to prevent scroll disclosure.
+ select->SetSuggestedValue("v15");
+ ASSERT_TRUE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(0.0, select->scrollTop());
+
+ // Clearing the preview resets the scroll position to the first selectable
+ // option.
+ select->SetSuggestedValue("");
+ ASSERT_FALSE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(0.0, select->scrollTop());
+}
+
TEST_F(HTMLSelectElementTest, SaveRestoreSelectSingleFormControlState) {
SetHtmlInnerHTML(
"<!DOCTYPE HTML><select id='sel'>"
diff --git a/third_party/blink/web_tests/fast/forms/select/select-autofill-popover-preview.html b/third_party/blink/web_tests/fast/forms/select/select-autofill-popover-preview.html
new file mode 100644
index 0000000..6fdbab4
--- /dev/null
+++ b/third_party/blink/web_tests/fast/forms/select/select-autofill-popover-preview.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jochen@chromium.org">
+<link rel=help href="https://issues.chromium.org/issues/40060525">
+<link rel=help href="https://issues.chromium.org/issues/40057398">
+<script src="../../../resources/testharness.js"></script>
+<script src="../../../resources/testharnessreport.js"></script>
+<script src="../resources/common.js"></script>
+<style>
+ @font-face {
+ font-family: Ahem;
+ src: url(../../../resources/Ahem.ttf);
+ }
+ .use-ahem {
+ font-family: Ahem;
+ font-size: 40px;
+ font-feature-settings: "case", "ss01";
+ width: 200px;
+ height: 100px;
+ }
+</style>
+
+<select id="menulist" class="use-ahem">
+ <option value="v1">Value 1</option>
+ <option value="v2">Value 2</option>
+</select>
+
+<select id="listbox" class="use-ahem" size="4">
+ <option value="v1">Value 1</option>
+ <option value="v2">Value 2</option>
+</select>
+
+<select id="listbox-scrollable" size="4">
+ <option value="v1">Value 1</option>
+ <option value="v2">Value 2</option>
+ <option value="v3">Value 3</option>
+ <option value="v4">Value 4</option>
+ <option value="v5">Value 5</option>
+ <option value="v6">Value 6</option>
+ <option value="v7">Value 7</option>
+ <option value="v8">Value 8</option>
+</select>
+
+<script>
+function testAutofillPopoverPreview(id, description) {
+ promise_test(async () => {
+ await document.fonts.ready;
+ const select = document.getElementById(id);
+ assert_true(!!select, `Element ${id} should exist`);
+
+ const initialStyle = window.getComputedStyle(select);
+ assert_equals(initialStyle.fontFamily, 'Ahem', `${id} should use author font initially`);
+
+ internals.setSuggestedValue(select, 'v2');
+
+ const shadowRoot = internals.shadowRoot(select);
+ const popover = getElementByPseudoId(shadowRoot, '-internal-select-autofill-preview');
+ assert_true(!!popover, `${id} autofill_popover_ element should exist in shadow DOM`);
+ assert_true(popover.matches(':popover-open'), `${id} autofill_popover_ should be open in preview state`);
+ assert_equals(window.getComputedStyle(popover).display, 'grid', `${id} autofill_popover_ display should be grid when open`);
+
+ // Full-element anchor sizing dimensions
+ assert_equals(window.getComputedStyle(popover).positionArea, 'center', `${id} autofill_popover_ position-area should be center`);
+ assert_equals(popover.offsetWidth, select.offsetWidth, `${id} autofill_popover_ width should match anchor select width`);
+ assert_equals(popover.offsetHeight, select.offsetHeight, `${id} autofill_popover_ height should match anchor select height`);
+
+ // Text-align start and text content
+ const popoverText = getElementByPseudoId(shadowRoot, '-internal-select-autofill-preview-text');
+ assert_true(!!popoverText, `${id} autofill_popover_text_ should exist in shadow DOM`);
+ assert_equals(popoverText.textContent, 'Value 2', `${id} autofill_popover_text_ should contain the label of the previewed option`);
+ const textStyle = window.getComputedStyle(popoverText);
+ assert_equals(textStyle.textAlign, 'start', `${id} autofill_popover_text_ should have text-align: start`);
+
+ // -webkit-small-control font pinning
+ assert_not_equals(textStyle.fontFamily, 'Ahem', `${id} autofill_popover_text_ should not use author font family in preview state`);
+ assert_not_equals(textStyle.fontSize, '40px', `${id} autofill_popover_text_ should pin font size to -webkit-small-control`);
+ assert_equals(textStyle.fontFeatureSettings, 'normal', `${id} autofill_popover_text_ should pin font-feature-settings to normal`);
+
+ // End preview
+ internals.setSuggestedValue(select, '');
+ assert_false(popover.matches(':popover-open'), `${id} autofill_popover_ should close when preview ends`);
+ assert_equals(window.getComputedStyle(popover).display, 'none', `${id} autofill_popover_ display should be none when closed`);
+ }, description);
+}
+
+testAutofillPopoverPreview('menulist', 'Autofill popover preview on menulist <select>');
+testAutofillPopoverPreview('listbox', 'Autofill popover preview on listbox <select>');
+
+test(() => {
+ const select = document.getElementById('listbox-scrollable');
+ assert_equals(select.scrollTop, 0);
+
+ // Previewing an option beyond the visible items does not scroll the listbox.
+ internals.setSuggestedValue(select, 'v8');
+ assert_equals(select.scrollTop, 0, 'scrollTop should remain 0 when previewing off-screen option');
+
+ internals.setSuggestedValue(select, '');
+ assert_equals(select.scrollTop, 0, 'scrollTop should remain 0 after clearing preview');
+
+ // User scrolls the listbox.
+ select.scrollTop = 30;
+ const userScrollTop = select.scrollTop;
+ assert_greater_than(userScrollTop, 0, 'listbox should be scrolled');
+
+ // Previewing does not modify or mask the user scroll position.
+ internals.setSuggestedValue(select, 'v8');
+ assert_equals(select.scrollTop, userScrollTop, 'scrollTop should match user scroll position during preview');
+
+ // Ending preview preserves the user scroll position without resetting.
+ internals.setSuggestedValue(select, '');
+ assert_equals(select.scrollTop, userScrollTop, 'scrollTop should be preserved after clearing preview');
+}, 'Autofill popover preview does not scroll or reset listbox scroll position');
+</script>
+
diff --git a/third_party/blink/web_tests/fast/forms/select/select-autofill-scroll-oracle.html b/third_party/blink/web_tests/fast/forms/select/select-autofill-scroll-oracle.html
new file mode 100644
index 0000000..6088baf7
--- /dev/null
+++ b/third_party/blink/web_tests/fast/forms/select/select-autofill-scroll-oracle.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<link rel="author" href="mailto:jochen@chromium.org">
+<link rel="help" href="https://issues.chromium.org/issues/40060525">
+<script src="../../../resources/testharness.js"></script>
+<script src="../../../resources/testharnessreport.js"></script>
+<script src="../resources/common.js"></script>
+<style>
+ select {
+ width: 200px;
+ height: 100px;
+ }
+</style>
+
+<select id="listbox" size="4">
... (truncated)
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