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

Overview

Medium
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactObservable discrepancy in Autofill
DescriptionObservable discrepancy in Autofill
ComponentAutofill
Sub-componentAutofill
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.
Tracker40057398
Fix commitb12935e03a81 (chromium/src) +429/-58
CWECWE-203
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedYoung Min Kim (@ylemkimon), CompSec Lab at Seoul National University
Reported2021-09-25 · bounty $2,000 (per vendor advisory)
Fix landed2026-08-10 (public git)
Disclosed2026-08-25
Patch gap15 days — fix public in source before the advisory

Background

Autofill preview
When a user hovers over or arrows through an Autofill suggestion, Blink puts the form control in a “previewed” state (IsPreviewed()) and shows the suggested value. That value must stay hidden from the page until the user accepts it.
`MenuListSelectType` / `ListBoxSelectType`
These are Blink’s implementations of the drop-down <select> and the <select size=N> list box; each builds a user-agent shadow tree for the control.
`SelectAutofillPreviewElement` (`autofill_popover_`)
This is a UA-shadow popover=manual top-layer element with an inner text div (autofill_popover_text_) that shows the previewed option’s label. Before this patch it was only used in appearance: base-select mode.
`SelectAutofillPopoverPreview`
This is a new runtime feature, added with status: "stable" (so on by default), that routes every <select> preview through that popover overlay.

Root Cause Analysis

Before the patch, a standard (non-base-select) menulist previewed a suggestion by returning select_->suggested_option_ from OptionToBeShown(). That wrote the unaccepted value into the control’s own inner element, which page CSS styles and lays out. A list box previewed by scrolling to the suggested option and relied on spot mitigations to hide that: Element’s scroll-offset getter returned 0, and the scroll-marker group kept the old selected_marker_. Information about the suggested value could therefore reach the page through observable side effects such as layout, rendering and scroll state, and the regression test’s author @font-face/font-feature-settings setup and the select-autofill-scroll-oracle.html test hint at exactly this.

The fix renders the preview only in the top-layer autofill_popover_ for both select types. With the flag on, OptionToBeShown() no longer returns the suggested option, ListBoxSelectType::DidSetSuggestedOption no longer scrolls, and the popover text uses font: -webkit-small-control !important so author fonts cannot be used to measure it. Because the list box no longer scrolls during preview, the patch turns off the older scrollTop-masking and scroll-marker freeze when the flag is enabled. That is a deliberate relaxation of those checks, and it is safe only because the underlying scroll no longer happens.

Key insight
The unaccepted Autofill value was placed in page-observable parts of the <select>: the menulist’s inner text and the list box’s scroll position. Spot-masking individual read paths could not close every channel. The fix moves the preview into a separate UA-styled top-layer popover, so the control’s own state never changes during preview.

Attack Path

  1. Page builds a probing form An attacker page contains <select> fields that Autofill will fill, such as address fields, and styles them with author fonts or scroll-observable layouts. This mirrors the regression web tests.
  2. User previews a suggestion The user focuses a field and hovers over a stored Autofill suggestion without accepting it. Chrome then calls SetSuggestedValue on the selects, which needs user interaction and saved Autofill data.
  3. Suggested value changes the control Before the patch, a menulist showed the suggested option’s text in its styled inner element, and a list box scrolled to the suggested option.
  4. Page observes the side effect Through rendering, layout, font or scroll side channels not covered by the existing scrollTop/scroll-marker masks, the page could infer the previewed value before the user consented. The specific channel used is (inferred).

Impact Assessment

The advisory classifies this as an observable discrepancy (information disclosure) in Autofill and gives no exploitation status here. A malicious web page could learn some or all of a user’s stored Autofill values for <select> fields, such as country or state, in the renderer, without the user accepting the fill. This requires the user to interact with the Autofill dropdown on that page and to have matching stored data. Which values are recoverable, and how precisely, depends on the side channel used (inferred).

Changed Functions

FunctionChangeNotes
GetAutofillPreviewElement
third_party/blink/renderer/core/html/forms/select_type.cc
modified
if
third_party/blink/renderer/core/html/forms/select_type.cc
modified
TEST_F
third_party/blink/renderer/core/html/forms/html_select_element_test.cc
modified

Files Changed

  • third_party/blink/renderer/platform/runtime_enabled_features.json5
  • third_party/blink/renderer/core/html/forms/select_type.cc
  • third_party/blink/web_tests/fast/forms/suggested-value-after-empty-suggested-value-expected.txt
  • third_party/blink/web_tests/fast/forms/select/select-autofill-popover-preview.html
  • third_party/blink/renderer/core/html/resources/html.css
  • third_party/blink/renderer/core/dom/scroll_marker_group_data.cc
  • third_party/blink/renderer/core/html/forms/select_type.h
  • third_party/blink/web_tests/fast/forms/select/select-autofill-scroll-oracle.html
  • third_party/blink/renderer/core/html/forms/html_select_element_test.cc
  • third_party/blink/renderer/core/dom/element.cc

Audit Directions

  • Preview state written into page-visible DOM or layout
    Check <input>, <textarea> and other Autofill or password-manager preview paths for suggested values that reach author-styled nodes, layout sizes, scroll offsets, :autofill/:has()-observable state or resize/scroll events.
  • Spot masks versus removing the side effect
    Wherever a getter is special-cased for IsPreviewed() (scrollTop, scroll markers, IntersectionObserver, scroll-driven animations), look for other read paths such as scrollLeft, getBoundingClientRect, scroll events and container queries that expose the same state.
  • Author style reaching UA preview text
    Confirm that preview elements pin font, size and feature settings with !important and cannot be targeted by author pseudo-elements, since @font-face ligatures or unicode-range loads can turn text into an observable signal (inferred).
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/select_type.cc b/third_party/blink/renderer/core/html/forms/select_type.cc
index 05d7f972..5886114 100644
--- a/third_party/blink/renderer/core/html/forms/select_type.cc
+++ b/third_party/blink/renderer/core/html/forms/select_type.cc
@@ -294,8 +294,6 @@
   bool IsAppearanceBasePicker() const override;
   bool PickerIsPopover() const override;
   void SetIsAppearanceBasePickerForDisplayNone(bool) override;
-  HTMLSelectElement::SelectAutofillPreviewElement* GetAutofillPreviewElement()
-      const override;
   Element& InnerElement() const override;
   void ShowPopup(PopupMenu::ShowEventType type) override;
   void HidePopup(SelectPopupHideBehavior) override;
@@ -325,8 +323,6 @@
   Member<const ComputedStyle> option_style_;
   Member<HTMLSlotElement> button_slot_;
   Member<PopoverElementForAppearanceBase> popover_;
-  Member<HTMLSelectElement::SelectAutofillPreviewElement> autofill_popover_;
-  Member<HTMLDivElement> autofill_popover_text_;
   Member<HTMLSlotElement> popover_input_slot_;
   Member<HTMLSlotElement> popover_options_slot_;
   // TODO(40146374): The option_slot_ can be removed when the CustomizableSelect
@@ -354,8 +350,6 @@
   visitor->Trace(option_style_);
   visitor->Trace(button_slot_);
   visitor->Trace(popover_);
-  visitor->Trace(autofill_popover_);
-  visitor->Trace(autofill_popover_text_);
   visitor->Trace(popover_input_slot_);
   visitor->Trace(popover_options_slot_);
   visitor->Trace(option_slot_);
@@ -620,18 +614,9 @@
   popover_options_slot_ = MakeGarbageCollected<HTMLSlotElement>(doc);
   popover_options_slot_->SetIdAttribute(shadow_element_names::kSelectOptions);
 
-  autofill_popover_ =
-      MakeGarbageCollected<HTMLSelectElement::SelectAutofillPreviewElement>(
-          doc, select_);
-  autofill_popover_->SetShadowPseudoId(
-      shadow_element_names::kSelectAutofillPreview);
-  root.appendChild(autofill_popover_);
-  autofill_popover_->setAttribute(html_names::kPopoverAttr, keywords::kManual);
-
-  autofill_popover_text_ = MakeGarbageCollected<HTMLDivElement>(doc);
-  autofill_popover_text_->SetShadowPseudoId(
-      shadow_element_names::kSelectAutofillPreviewText);
-  autofill_popover_->appendChild(autofill_popover_text_);
+  if (!RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled()) {
+    CreateAutofillPopover(root);
+  }
 
   if (RuntimeEnabledFeatures::FilterableSelectEnabled() &&
       select_->NumDescendantInputs()) {
@@ -662,6 +647,10 @@
     popover_input_slot_ = nullptr;
     popover_->AppendChild(popover_options_slot_);
   }
+
+  if (RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled()) {
+    CreateAutofillPopover(root);
+  }
 }
 
 void MenuListSelectType::ManuallyAssignSlots() {
@@ -779,11 +768,6 @@
   is_appearance_base_picker_for_display_none_ = value;
 }
 
-HTMLSelectElement::SelectAutofillPreviewElement*
-MenuListSelectType::GetAutofillPreviewElement() const {
-  return autofill_popover_;
-}
-
 Element& MenuListSelectType::InnerElement() const {
   return *inner_element_;
 }
@@ -1001,17 +985,9 @@
   if (native_popup_is_visible_) {
     popup_->UpdateFromElement(PopupMenu::kBySelectionChange);
   }
-  if (select_->IsAppearanceBase()) {
-    if (option) {
-      autofill_popover_->ShowPopoverInternal(select_, &ASSERT_NO_EXCEPTION);
-      autofill_popover_text_->setInnerText(option->label());
-    } else {
-      autofill_popover_text_->setInnerText(g_empty_string);
-      autofill_popover_->HidePopoverInternal(
-          /*invoker=*/nullptr, HidePopoverFocusBehavior::kNone,
-          HidePopoverTransitionBehavior::kNoEventsNoWaiting,
-          /*exception_state=*/nullptr);
-    }
+  if (RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled() ||
+      select_->IsAppearanceBase()) {
+    SelectType::DidSetSuggestedOption(option);
   }
 }
 
@@ -1175,7 +1151,8 @@
     return option;
   // In appearance:base-select mode, we don't want to reveal the suggested
   // option anywhere except in autofill_popover_.
-  if (select_->suggested_option_ && !select_->IsAppearanceBase()) {
+  if (!RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled() &&
+      select_->suggested_option_ && !select_->IsAppearanceBase()) {
     return select_->suggested_option_.Get();
   }
   // TODO(tkent): We should not call OptionToBeShown() in IsMultiple() case.
@@ -1303,8 +1280,6 @@
   bool IsAppearanceBasePicker() const override;
   bool PickerIsPopover() const override;
   void SetIsAppearanceBasePickerForDisplayNone(bool) override;
-  HTMLSelectElement::SelectAutofillPreviewElement* GetAutofillPreviewElement()
-      const override;
   void DidRecalcStyle(const StyleRecalcChange) override;
 
  private:
@@ -1664,6 +1639,10 @@
 void ListBoxSelectType::DidSetSuggestedOption(HTMLOptionElement* option) {
   if (!select_->GetLayoutObject())
     return;
+  if (RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled()) {
+    SelectType::DidSetSuggestedOption(option);
+    return;
+  }
   // When ending preview state, don't leave the scroll position at the
   // previewed element but return to the active selection end if it is
   // defined or to the first selectable option. See crbug.com/1261689.
@@ -1996,6 +1975,8 @@
     root.appendChild(option_slot_);
     listbox_ = nullptr;
   }
+
+  CreateAutofillPopover(root);
 }
 
 void ListBoxSelectType::ManuallyAssignSlots() {
@@ -2052,12 +2033,6 @@
                   "UsesMenuList(). ListBoxSelectType does not have a picker.";
 }
 
-HTMLSelectElement::SelectAutofillPreviewElement*
-ListBoxSelectType::GetAutofillPreviewElement() const {
-  // TODO(crbug.com/357649033): Implement this
-  return nullptr;
-}
-
 void ListBoxSelectType::DidRecalcStyle(const StyleRecalcChange) {
   UpdateOptionMutationObservers(/*in_style_recalc=*/true);
 }
@@ -2108,6 +2083,50 @@
 
 void SelectType::Trace(Visitor* visitor) const {
   visitor->Trace(select_);
+  visitor->Trace(autofill_popover_);
+  visitor->Trace(autofill_popover_text_);
+}
+
+void SelectType::CreateAutofillPopover(ShadowRoot& root) {
+  if (!RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled() &&
+      !select_->IsAppearanceBase()) {
+    return;
+  }
+  select_->SetMayBeImplicitAnchor();
+  Document& doc = select_->GetDocument();
+  autofill_popover_ =
+      MakeGarbageCollected<HTMLSelectElement::SelectAutofillPreviewElement>(
+          doc, select_);
+  autofill_popover_->SetShadowPseudoId(
+      shadow_element_names::kSelectAutofillPreview);
+  root.appendChild(autofill_popover_);
+  autofill_popover_->setAttribute(html_names::kPopoverAttr, keywords::kManual);
+
+  autofill_popover_text_ = MakeGarbageCollected<HTMLDivElement>(doc);
+  autofill_popover_text_->SetShadowPseudoId(
+      shadow_element_names::kSelectAutofillPreviewText);
+  autofill_popover_->appendChild(autofill_popover_text_);
+}
+
+HTMLSelectElement::SelectAutofillPreviewElement*
+SelectType::GetAutofillPreviewElement() const {
+  return autofill_popover_;
+}
+
+void SelectType::DidSetSuggestedOption(HTMLOptionElement* option) {
+  if (!autofill_popover_) {
+    return;
+  }
+  if (option) {
+    autofill_popover_->ShowPopoverInternal(select_, &ASSERT_NO_EXCEPTION);
+    autofill_popover_text_->setInnerText(option->label());
+  } else {
+    autofill_popover_text_->setInnerText(g_empty_string);
+    autofill_popover_->HidePopoverInternal(
+        /*invoker=*/nullptr, HidePopoverFocusBehavior::kNone,
+        HidePopoverTransitionBehavior::kNoEventsNoWaiting,
+        /*exception_state=*/nullptr);
+  }
 }
 
 void SelectType::OptionRemoved(HTMLOptionElement& option) {}
diff --git a/third_party/blink/renderer/core/html/forms/select_type.h b/third_party/blink/renderer/core/html/forms/select_type.h
index aba5b06..3c04fd057 100644
--- a/third_party/blink/renderer/core/html/forms/select_type.h
+++ b/third_party/blink/renderer/core/html/forms/select_type.h
@@ -35,7 +35,7 @@
   virtual void DidBlur() = 0;
   virtual void DidDetachLayoutTree();
   virtual void DidRecalcStyle(const StyleRecalcChange change);
-  virtual void DidSetSuggestedOption(HTMLOptionElement* option) = 0;
+  virtual void DidSetSuggestedOption(HTMLOptionElement* option);
   virtual void SaveLastSelection() = 0;
 
   // Update style of text in the CSS box on style or selected OPTION change.
@@ -72,7 +72,7 @@
   virtual bool PickerIsPopover() const = 0;
   virtual void SetIsAppearanceBasePickerForDisplayNone(bool) = 0;
   virtual HTMLSelectElement::SelectAutofillPreviewElement*
-  GetAutofillPreviewElement() const = 0;
+  GetAutofillPreviewElement() const;
   virtual Element& InnerElement() const;
   virtual void ShowPopup(PopupMenu::ShowEventType type);
   virtual void HidePopup(SelectPopupHideBehavior);
@@ -95,7 +95,11 @@
                                      SkipDirection direction,
                                      int skip) const;
 
+  void CreateAutofillPopover(ShadowRoot& root);
+
   const Member<HTMLSelectElement> select_;
+  Member<HTMLSelectElement::SelectAutofillPreviewElement> autofill_popover_;
+  Member<HTMLDivElement> autofill_popover_text_;
   bool will_be_destroyed_ = false;
 
  private:
diff --git a/third_party/blink/renderer/core/html/resources/html.css b/third_party/blink/renderer/core/html/resources/html.css
index 789de46..9bd97f6e 100644
--- a/third_party/blink/renderer/core/html/resources/html.css
+++ b/third_party/blink/renderer/core/html/resources/html.css
@@ -2355,7 +2355,8 @@
 select::-internal-select-autofill-preview-text {
   overflow: hidden;
   text-overflow: ellipsis;
-  text-align: center;
+  text-align: start;
+  padding-inline: -internal-auto-base(2px, 0.5em);
   white-space: nowrap;
   font: -webkit-small-control !important;
   /* See input::-internal-input-suggested: pin a generic family so an author
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index fa184ab7..9a56eac8 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -5473,6 +5473,10 @@
       },
     },
     {
+      name: "SelectAutofillPopoverPreview",
+      status: "stable",
+    },
+    {
       // The selectedcontentelement attribute is for connecting select elements
       // to arbitrary <selectedcontent> elements.
       // https://github.com/openui/open-ui/issues/1063
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);
 
... (file diff truncated)
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

reported by ym...@ym.kim

Security: font side-channel attack against <select> autofill preview discloses sensitive information

This issue is similar to https://crbug.com/chromium/1253101, but since the root bug is different and a slightly different attack is possible for <select>, I’ve opened another issue.

VULNERABILITY DETAILS
Tl;dr incorrect handling of the autofill preview font style on <select> can leak the content of the preview text, which can be activated via a single keypress or mouse action and contains sensitive information.

  1. Setting the font of <select> preview text
    The font pinning of preview text is not applied to the <select>. In crrev.com/669336, it’s stated that:
    > As a side effect, the font-family is only set for <input> elements but not for <textarea> or <select> elements, which should not be counter the reason for which crrev.com/640884 was implemented. Chrome does not fill <textarea> elements and <select> elements draw themselves entirely differently.

Therefore, the preview text inherits the font style of <select>.

* Proposed fix: enforce the style of <select> preview text, preferably in the renderer

  1. Detecting used characters
    Identical to https://crbug.com/chromium/1253101.

  2. Inferring the content
    (1) The credit card expiration date, U.S. states, countries, and most CJK words have an almost unique combination of used alphabets, so they can be reliably inferred.
    (2) A uniquely identifiable character, possibly a homograph or invisible character, can be added to <option>s and used to identify which option is suggested. This also makes it possible to get options from all autofill entries if the user is made to scroll through all entries, e.g., keep pressing the down arrow key.

Note the width of <select> should not be auto, as all options will be rendered to calculate the width, thus loading characters for all the options.

  1. Real-world scenarios and 5. related bugs
    Identical to https://crbug.com/chromium/1253101.

VERSION
Reproducible on:

  • 94.0.4606.61 stable, Windows 10 Version 21H1 (Build 19043.1237)
  • 96.0.4652.1 canary, Windows 10 Version 21H1 (Build 19043.1237)
  • 94.0.4606.61 stable, Linux 5.11.0-34-generic #40~20.04.1-Ubuntu SMP

REPRODUCTION CASE

  1. Add at least one credit card entry in chrome://settings/payments and/or one address entry in chrome://settings/addresses
  2. Serve the attached poc.html over a secure origin and navigate to the page (an online version is available in https://me94bk1usdv8c8tfybvy.netlify.app/iuaonzhy9xkblcodbzvw.html)
  3. Click on any input and hover the mouse over an autofill entry OR press the up or down arrow key on focused input to preview autofill.
  4. The inferred content will appear below.

Note PoCs above use Roboto font from Google Fonts, of which URL might change in the future.

CREDIT INFORMATION
Reporter credit: Young Min Kim (@ylemkimon), CompSec Lab at Seoul National University

View on issue tracker