CVE-2026-87593
Overview
Background
- `window.find()`
- the JavaScript/Ctrl-F API that searches the rendered text of a document, implemented in Blink’s finder by building a text buffer over the layout via
FindBuffer. - Suggested value (autofill preview)
- a preview string the browser paints inside a form control (
TextControlElement::SuggestedValue(),HTMLSelectElement::SuggestedValue()) to show a proposed autofill entry that has not been committed to the element’s real value. - `find_buffer.cc` skip helper
- a static predicate (enclosing function name is not shown in the hunk; it is
ShouldIgnoreContents(inferred)) that returnstruewhen a node’s contents must be excluded from the find buffer. - View resync
- an
HTMLInputElementpaints throughinput_type_view_, whose rendered text only refreshes whenDidSetValue()(on a real change) orUpdateView()runs.
Root Cause Analysis
The autofill preview text is deliberately excluded from window.find() so that private suggestions cannot be probed, but two paths let the preview stay searchable. First, the find_buffer.cc skip helper only excluded TextControlElements that had a non-empty SuggestedValue(); it had no case for HTMLSelectElement, so a listbox/dropdown <select> showing an autofill preview was still added to the find buffer. Second, in HTMLInputElement::SetValue (inferred; name not in the hunk) the code cleared the suggested value with the base-class TextControlElement::SetSuggestedValue(String()) “to not trigger a view update,” and then only refreshed rendering inside the value_changed branch — so when the newly committed value equalled the sanitized existing value (value_changed == false), the view kept rendering the just-cleared preview while SuggestedValue() was now empty, defeating the skip predicate.
The fix moves the text-control preview case to an early return true, adds an HTMLSelectElement preview case (gated on FindIgnoreSuggestionFix), and, in SetValue, records had_suggested_value before clearing and forces input_type_view_->UpdateView() when had_suggested_value && !value_changed, so the stale preview is repainted away before it can be searched.
SuggestedValue()” as equivalent to “no preview is on screen”: the exclusion missed <select> entirely and went stale on inputs whose preview was cleared without a repaint. The fix broadens the exclusion to selects and re-syncs the input’s rendering after clearing, keeping rendered previews and the find buffer consistent.Attack Path
- Present an autofill preview
A malicious page places a form control (
<input>, or a<select>per bug 553136688) that the browser fills with an autofill preview, causing sensitive suggestion text to be painted without being committed. - Force a stale render (input path)
Script sets
input.valueto a string that sanitizes to the element’s existing value sovalue_changedisfalse; the suggested value is cleared but the view is never resynced, leaving the preview text still rendered. For selects, no clearing is even needed — the preview was never skipped. - Probe with `window.find()`
The page calls
window.find(candidate)against the still-rendered preview; a hit versus miss reveals whethercandidatematches the private autofill text. - Exfiltrate character by character By iterating candidate substrings and observing find hits, the page reconstructs the autofill preview value (e.g. address, phone, or other stored profile data) without the user accepting the suggestion.
Impact Assessment
window.find() as an oracle, requiring only that the page can trigger an autofill preview and script the input’s value or read a <select> preview. This is an information disclosure of private autofill data as the advisory states (“Information leak in Editing”); no memory-safety escalation is demonstrated by the patch and any further impact would be (inferred). The advisory does not report exploitation in the wild.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/html/forms/html_input_element.cc |
modified | |
ifthird_party/blink/web_tests/fast/forms/suggested-value-do-not-search.html |
modified | |
forthird_party/blink/web_tests/fast/forms/suggested-value-do-not-search.html |
modified |
Files Changed
third_party/blink/renderer/core/editing/finder/find_buffer.ccthird_party/blink/renderer/platform/runtime_enabled_features.json5third_party/blink/web_tests/fast/forms/suggested-value-do-not-search.htmlthird_party/blink/renderer/core/html/forms/html_input_element.cc
Audit Directions
- Rendered-vs-model divergenceAudit any code that gates a repaint on a “value changed” flag after clearing preview/placeholder state; a no-op-looking assignment can leave sensitive rendered text out of sync with the element’s reported state.
- Incomplete element-type coverage in find/serialization filtersReview
find_buffer.ccand similar accessibility/serialization skip lists for form controls (<select>,<meter>,<progress>, custom controls) that expose preview or shadow-DOM text not covered by existingIsA<>cases. - `window.find()` as an oracleLook for other UI-only or privacy-sensitive text (autofill previews, placeholder, spellcheck,
::selection, shadow content) that reaches the layout tree and can be probed for presence viawindow.find()or range-based text APIs.
Patch
From 0a4abb0319803f0d07d3bd06dd49ee9089ce908c Mon Sep 17 00:00:00 2001
From: Kent Tamura <tkent@chromium.org>
Date: Sun, 30 Aug 2026 17:56:39 -0700
Subject: [PATCH] Fix issues on window.find() and Autofill suggested values
* <input> failed to update rendering after updating its value.
window.find() find text on the rendered text.
* window.find() should exclude listbox <select> with a suggested value.
Fixed: 553136688, 553252820
Change-Id: I58a7159cc7319e682205b64aa31348ee682a40c0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8313287
Commit-Queue: Kent Tamura <tkent@chromium.org>
Auto-Submit: Kent Tamura <tkent@chromium.org>
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1688783}
---
diff --git a/third_party/blink/renderer/core/editing/finder/find_buffer.cc b/third_party/blink/renderer/core/editing/finder/find_buffer.cc
index 4b925627..5d05bcce 100644
--- a/third_party/blink/renderer/core/editing/finder/find_buffer.cc
+++ b/third_party/blink/renderer/core/editing/finder/find_buffer.cc
@@ -71,10 +71,22 @@
const auto* element = DynamicTo<HTMLElement>(node);
if (!element)
return false;
+
+ // Skip elements showing autofill-preview.
+ if (IsA<TextControlElement>(*element) &&
+ !To<TextControlElement>(*element).SuggestedValue().empty()) {
+ return true;
+ }
+ if (RuntimeEnabledFeatures::FindIgnoreSuggestionFixEnabled()) {
+ if (const auto* select = DynamicTo<HTMLSelectElement>(element)) {
+ if (!select->SuggestedValue().empty()) {
+ return true;
+ }
+ }
+ }
+
return (!element->ShouldSerializeEndTag() &&
!IsA<HTMLInputElement>(*element)) ||
- (IsA<TextControlElement>(*element) &&
- !To<TextControlElement>(*element).SuggestedValue().empty()) ||
IsA<HTMLIFrameElement>(*element) || IsA<HTMLImageElement>(*element) ||
IsA<HTMLMeterElement>(*element) || IsA<HTMLObjectElement>(*element) ||
IsA<HTMLProgressElement>(*element) ||
diff --git a/third_party/blink/renderer/core/html/forms/html_input_element.cc b/third_party/blink/renderer/core/html/forms/html_input_element.cc
index dd28a7a0..3b0f78ef 100644
--- a/third_party/blink/renderer/core/html/forms/html_input_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_input_element.cc
@@ -1417,6 +1417,9 @@
if (!input_type_->CanSetValue(value))
return;
+ const bool had_suggested_value =
+ RuntimeEnabledFeatures::FindIgnoreSuggestionFixEnabled() &&
+ !SuggestedValue().empty();
// Clear the suggested value. Use the base class version to not trigger a view
// update.
TextControlElement::SetSuggestedValue(String());
@@ -1437,6 +1440,12 @@
selection);
input_type_view_->DidSetValue(sanitized_value, value_changed);
+ if (had_suggested_value && !value_changed) {
+ // The view may still render the just-cleared suggested value; force a
+ // resync to the committed value. crbug.com/553252820
+ input_type_view_->UpdateView();
+ }
+
if (value_changed) {
NotifyFormStateChanged();
if (sanitized_value.empty() && HasBeenPasswordField() &&
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index 79111ca..0661ddc 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -3012,6 +3012,11 @@
status: "stable",
},
{
+ // crbug.com/553136688 and crbug.com/553252820
+ name: "FindIgnoreSuggestionFix",
+ status: "stable",
+ },
+ {
name: "FirstLineOnListItem",
status: "stable",
},
diff --git a/third_party/blink/web_tests/fast/forms/suggested-value-do-not-search.html b/third_party/blink/web_tests/fast/forms/suggested-value-do-not-search.html
index c998e33..74aa79ce 100644
--- a/third_party/blink/web_tests/fast/forms/suggested-value-do-not-search.html
+++ b/third_party/blink/web_tests/fast/forms/suggested-value-do-not-search.html
@@ -2,14 +2,25 @@
<html>
<body>
<title> Tests that a suggested value is not found by a text search. </title>
+
<input id='text' type='text'>
<textarea id='textarea'></textarea>
+<select id='listbox' size='2'></select>
+<select id='dropdown'></select>
+<style>
+option {
+ visibility: hidden;
+}
+</style>
+
<script src='../../resources/testharness.js'></script>
<script src='../../resources/testharnessreport.js'></script>
<script>
var input = document.getElementById('text');
var textarea = document.getElementById('textarea');
+const listbox = document.getElementById('listbox');
+const dropdown = document.getElementById('dropdown');
if (!window.internals) {
testFailed('This test requires internals object');
@@ -35,22 +46,30 @@
['image', 'nonsense'],
['radio', 'select-me'],
['range', '3'],
- ['week', '2075-W33'],
- ['date', '2075-05-01', ''],
- ['datetime-local', '2075-05-01T19:30'],
+ ['week', '2075-W33', '2075'],
+ ['date', '2075-05-01', '2075'],
+ ['datetime-local', '2075-05-01T19:30', '2075'],
// These do support suggested values.
- ['month', '2075-02'],
- ['text', 'blabla'],
- ['tel', '3'],
+ ['month', '2026-08', '2026'],
+ ['text', 'blabla', 'bla'],
+ ['tel', '12345', '234'],
];
- // Tests for different types that the value is not found if set as a suggested value.
- for ( const [type, value] of testcases ) {
+ // Tests for different types that the value is not found if set as a suggested value.
+ for (const [type, value, query = value] of testcases) {
+ input.value = '';
input.type = type;
internals.setSuggestedValue(input, value);
- assert_false(lookup(value));
- }
+ assert_false(lookup(query), `Suggested value query "${query}" in input[type=${type}] should not be found.`);
+ if (query !== value) {
+ assert_false(lookup(value), `Suggested value "${value}" in input[type=${type}] should not be found.`);
+ }
+ // crbug.com/553252820. Suggeted value should not be findable after it was cleared.
+ input.value = input.value;
+ assert_false(lookup(query), `Suggested value query "${query}" in input[type=${type}] should not be found after reset.`);
+ }
+ input.remove();
}, 'Test that suggested values in an input field is not found by search');
test(() => {
@@ -66,6 +85,32 @@
}, 'Test that suggested values in a text area are not found by search');
+ test(() => {
+ // Verify that options with visibility:hidden are not found in a dropdown select.
+ dropdown.innerHTML = '<option value="value">option-text</option><option>preview</option>';
+ assert_false(lookup('option-text'));
+ assert_false(lookup('option'));
+ assert_false(lookup('preview'));
+
+ // Verify that that 'preview' is not found if set as a suggested value.
+ internals.setSuggestedValue(dropdown, 'preview');
+ assert_false(lookup('preview'));
+ assert_false(lookup('view'));
+ }, 'Test that suggested values in a dropdown select are not found by search');
+
+ test(() => {
+ // Verify that options with visibility:hidden are not found in a listbox select.
+ listbox.innerHTML = '<option value="value">option-text</option><option>preview</option>';
+ assert_false(lookup('option-text'));
+ assert_false(lookup('option'));
+ assert_false(lookup('preview'));
+
+ // Verify that that 'preview' is not found if set as a suggested value.
+ internals.setSuggestedValue(listbox, 'preview');
+ assert_false(lookup('preview'));
+ assert_false(lookup('view'));
+ }, 'crbug.com/553136688: Test that suggested values in a listbox select are not found by search');
+
}
</script>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/fast/forms/suggested-value-do-not-search.html b/third_party/blink/web_tests/fast/forms/suggested-value-do-not-search.html
index c998e33..74aa79ce 100644
--- a/third_party/blink/web_tests/fast/forms/suggested-value-do-not-search.html
+++ b/third_party/blink/web_tests/fast/forms/suggested-value-do-not-search.html
@@ -2,14 +2,25 @@
<html>
<body>
<title> Tests that a suggested value is not found by a text search. </title>
+
<input id='text' type='text'>
<textarea id='textarea'></textarea>
+<select id='listbox' size='2'></select>
+<select id='dropdown'></select>
+<style>
+option {
+ visibility: hidden;
+}
+</style>
+
<script src='../../resources/testharness.js'></script>
<script src='../../resources/testharnessreport.js'></script>
<script>
var input = document.getElementById('text');
var textarea = document.getElementById('textarea');
+const listbox = document.getElementById('listbox');
+const dropdown = document.getElementById('dropdown');
if (!window.internals) {
testFailed('This test requires internals object');
@@ -35,22 +46,30 @@
['image', 'nonsense'],
['radio', 'select-me'],
['range', '3'],
- ['week', '2075-W33'],
- ['date', '2075-05-01', ''],
- ['datetime-local', '2075-05-01T19:30'],
+ ['week', '2075-W33', '2075'],
+ ['date', '2075-05-01', '2075'],
+ ['datetime-local', '2075-05-01T19:30', '2075'],
// These do support suggested values.
- ['month', '2075-02'],
- ['text', 'blabla'],
- ['tel', '3'],
+ ['month', '2026-08', '2026'],
+ ['text', 'blabla', 'bla'],
+ ['tel', '12345', '234'],
];
- // Tests for different types that the value is not found if set as a suggested value.
- for ( const [type, value] of testcases ) {
+ // Tests for different types that the value is not found if set as a suggested value.
+ for (const [type, value, query = value] of testcases) {
+ input.value = '';
input.type = type;
internals.setSuggestedValue(input, value);
- assert_false(lookup(value));
- }
+ assert_false(lookup(query), `Suggested value query "${query}" in input[type=${type}] should not be found.`);
+ if (query !== value) {
+ assert_false(lookup(value), `Suggested value "${value}" in input[type=${type}] should not be found.`);
+ }
+ // crbug.com/553252820. Suggeted value should not be findable after it was cleared.
+ input.value = input.value;
+ assert_false(lookup(query), `Suggested value query "${query}" in input[type=${type}] should not be found after reset.`);
+ }
+ input.remove();
}, 'Test that suggested values in an input field is not found by search');
test(() => {
@@ -66,6 +85,32 @@
}, 'Test that suggested values in a text area are not found by search');
+ test(() => {
+ // Verify that options with visibility:hidden are not found in a dropdown select.
+ dropdown.innerHTML = '<option value="value">option-text</option><option>preview</option>';
+ assert_false(lookup('option-text'));
+ assert_false(lookup('option'));
+ assert_false(lookup('preview'));
+
+ // Verify that that 'preview' is not found if set as a suggested value.
+ internals.setSuggestedValue(dropdown, 'preview');
+ assert_false(lookup('preview'));
+ assert_false(lookup('view'));
+ }, 'Test that suggested values in a dropdown select are not found by search');
+
+ test(() => {
+ // Verify that options with visibility:hidden are not found in a listbox select.
+ listbox.innerHTML = '<option value="value">option-text</option><option>preview</option>';
+ assert_false(lookup('option-text'));
+ assert_false(lookup('option'));
+ assert_false(lookup('preview'));
+
+ // Verify that that 'preview' is not found if set as a suggested value.
+ internals.setSuggestedValue(listbox, 'preview');
+ assert_false(lookup('preview'));
+ assert_false(lookup('view'));
+ }, 'crbug.com/553136688: Test that suggested values in a listbox select are not found by search');
+
}
</script>