CVE-2026-106321
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/core/editing/iterators/text_iterator_test.cc |
modified |
Files Changed
third_party/blink/renderer/platform/runtime_enabled_features.json5third_party/blink/renderer/core/editing/iterators/text_iterator_test.ccthird_party/blink/renderer/core/editing/iterators/text_iterator.cc
Patch
From 3ceb583e03c61aa4e531ff8399838bcfe9e015f1 Mon Sep 17 00:00:00 2001
From: Kent Tamura <tkent@chromium.org>
Date: Thu, 03 Sep 2026 21:04:09 -0700
Subject: [PATCH] Exclude autofilled <select> preview value in TextIterator
TextIteratorAlgorithm<Strategy>::HandleTextNode() checks
EnclosingTextControl(node_) to exclude autofilled values when
ExcludesAutofilledValue() is true. However, EnclosingTextControl() only
finds TextControlElement (<input> and <textarea>), missing
HTMLSelectElement whose autofill preview value is rendered inside a
user-agent shadow root (popover).
This CL updates HandleTextNode() to recognize HTMLSelectElement in UA
shadow roots and exclude its autofilled preview value.
This behavior is guarded by a new runtime feature flag
TextIteratorExcludeAutofilledSelectFix (status: stable).
Bug: 556235808
Change-Id: I1bbfaf1e743291276ddf46db4aa7d58f66fba7be
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8351184
Commit-Queue: Koji Ishii <kojii@chromium.org>
Reviewed-by: Koji Ishii <kojii@chromium.org>
Auto-Submit: Kent Tamura <tkent@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1692167}
---
diff --git a/third_party/blink/renderer/core/editing/iterators/text_iterator.cc b/third_party/blink/renderer/core/editing/iterators/text_iterator.cc
index ca393f3a..206d7bf 100644
--- a/third_party/blink/renderer/core/editing/iterators/text_iterator.cc
+++ b/third_party/blink/renderer/core/editing/iterators/text_iterator.cc
@@ -45,6 +45,7 @@
#include "third_party/blink/renderer/core/html/forms/html_legend_element.h"
#include "third_party/blink/renderer/core/html/forms/html_opt_group_element.h"
#include "third_party/blink/renderer/core/html/forms/html_option_element.h"
+#include "third_party/blink/renderer/core/html/forms/html_select_element.h"
#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
#include "third_party/blink/renderer/core/html/html_body_element.h"
#include "third_party/blink/renderer/core/html/html_element.h"
@@ -533,7 +534,15 @@
template <typename Strategy>
void TextIteratorAlgorithm<Strategy>::HandleTextNode() {
if (ExcludesAutofilledValue()) {
- TextControlElement* control = EnclosingTextControl(node_);
+ HTMLFormControlElement* control = nullptr;
+ if (RuntimeEnabledFeatures::
+ TextIteratorExcludeAutofilledSelectFixEnabled()) {
+ if (node_->IsInUserAgentShadowRoot()) {
+ control = DynamicTo<HTMLFormControlElement>(node_->OwnerShadowHost());
+ }
+ } else {
+ control = EnclosingTextControl(node_);
+ }
// For security reason, we don't expose suggested value if it is
// auto-filled.
// TODO(crbug.com/1472209): Only hide suggested value of previews.
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index b1b5fe4..1b6457d 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -6325,6 +6325,11 @@
status: {"Android": "stable"},
},
{
+ // crbug.com/556235808
+ name: "TextIteratorExcludeAutofilledSelectFix",
+ status: "stable",
+ },
+ {
name: "TextMetricsBaselines",
status: "stable",
},
diff --git a/third_party/blink/renderer/core/editing/iterators/text_iterator_test.cc b/third_party/blink/renderer/core/editing/iterators/text_iterator_test.cc
index 7f55a9f..1b4f519 100644
--- a/third_party/blink/renderer/core/editing/iterators/text_iterator_test.cc
+++ b/third_party/blink/renderer/core/editing/iterators/text_iterator_test.cc
@@ -36,7 +36,10 @@
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/html/forms/html_select_element.h"
#include "third_party/blink/renderer/core/html/forms/text_control_element.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 {
@@ -1177,5 +1180,71 @@
EXPECT_EQ("[test][][;]", Iterate<FlatTree>());
}
+TEST_F(TextIteratorTest, ExcludeAutofilledValueSelect) {
+ SetBodyContent(
+ "<select id='sel'>"
+ "<option value='1' selected>One</option>"
+ "<option value='2'>Two</option>"
+ "</select>");
+ auto* select = To<HTMLSelectElement>(GetElementById("sel"));
+ select->SetSuggestedValue("2");
+ ASSERT_TRUE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+
+ Element* popover = select->GetAutofillPreviewElement();
+ ASSERT_TRUE(popover);
+
+ const Position start = Position::FirstPositionInNode(*popover);
+ const Position end = Position::LastPositionInNode(*popover);
+
+ // Without ExcludeAutofilledValue, the preview text in the popover is emitted.
+ EXPECT_EQ("[Two]", IteratePartial<DomTree>(start, end));
+
+ // With ExcludeAutofilledValue, the preview text must be suppressed when the
+ // fix is enabled.
+ TextIteratorBehavior behavior =
+ TextIteratorBehavior::Builder().SetExcludeAutofilledValue(true).Build();
+ EXPECT_EQ("", IteratePartial<DomTree>(start, end, behavior));
+
+ // When the feature is disabled, the previous behavior is restored (preview
+ // text is emitted).
+ ScopedTextIteratorExcludeAutofilledSelectFixForTest disable_fix(false);
+ EXPECT_EQ("[Two]", IteratePartial<DomTree>(start, end, behavior));
+}
+
+TEST_F(TextIteratorTest, ExcludeAutofilledValueListboxSelect) {
+ SetBodyContent(
+ "<select id='sel' size='2'>"
+ "<option value='1' selected>One</option>"
+ "<option value='2'>Two</option>"
+ "</select>");
+ auto* select = To<HTMLSelectElement>(GetElementById("sel"));
+ select->SetSuggestedValue("2");
+ ASSERT_TRUE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+
+ Element* popover = select->GetAutofillPreviewElement();
+ ASSERT_TRUE(popover);
+
+ const Position start = Position::FirstPositionInNode(*popover);
+ const Position end = Position::LastPositionInNode(*popover);
+
+ // Without ExcludeAutofilledValue, the preview text in the popover is emitted.
+ EXPECT_EQ("[Two]", IteratePartial<DomTree>(start, end));
+
+ // With ExcludeAutofilledValue, the preview text must be suppressed when the
+ // fix is enabled.
+ TextIteratorBehavior behavior =
+ TextIteratorBehavior::Builder().SetExcludeAutofilledValue(true).Build();
+ EXPECT_EQ("", IteratePartial<DomTree>(start, end, behavior));
+
+ // When the feature is disabled, the previous behavior is restored (preview
+ // text is emitted).
+ ScopedTextIteratorExcludeAutofilledSelectFixForTest disable_fix(false);
+ EXPECT_EQ("[Two]", IteratePartial<DomTree>(start, end, behavior));
+}
+
} // namespace text_iterator_test
} // namespace blink
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/editing/iterators/text_iterator_test.cc b/third_party/blink/renderer/core/editing/iterators/text_iterator_test.cc
index 7f55a9f..1b4f519 100644
--- a/third_party/blink/renderer/core/editing/iterators/text_iterator_test.cc
+++ b/third_party/blink/renderer/core/editing/iterators/text_iterator_test.cc
@@ -36,7 +36,10 @@
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/html/forms/html_select_element.h"
#include "third_party/blink/renderer/core/html/forms/text_control_element.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 {
@@ -1177,5 +1180,71 @@
EXPECT_EQ("[test][][;]", Iterate<FlatTree>());
}
+TEST_F(TextIteratorTest, ExcludeAutofilledValueSelect) {
+ SetBodyContent(
+ "<select id='sel'>"
+ "<option value='1' selected>One</option>"
+ "<option value='2'>Two</option>"
+ "</select>");
+ auto* select = To<HTMLSelectElement>(GetElementById("sel"));
+ select->SetSuggestedValue("2");
+ ASSERT_TRUE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+
+ Element* popover = select->GetAutofillPreviewElement();
+ ASSERT_TRUE(popover);
+
+ const Position start = Position::FirstPositionInNode(*popover);
+ const Position end = Position::LastPositionInNode(*popover);
+
+ // Without ExcludeAutofilledValue, the preview text in the popover is emitted.
+ EXPECT_EQ("[Two]", IteratePartial<DomTree>(start, end));
+
+ // With ExcludeAutofilledValue, the preview text must be suppressed when the
+ // fix is enabled.
+ TextIteratorBehavior behavior =
+ TextIteratorBehavior::Builder().SetExcludeAutofilledValue(true).Build();
+ EXPECT_EQ("", IteratePartial<DomTree>(start, end, behavior));
+
+ // When the feature is disabled, the previous behavior is restored (preview
+ // text is emitted).
+ ScopedTextIteratorExcludeAutofilledSelectFixForTest disable_fix(false);
+ EXPECT_EQ("[Two]", IteratePartial<DomTree>(start, end, behavior));
+}
+
+TEST_F(TextIteratorTest, ExcludeAutofilledValueListboxSelect) {
+ SetBodyContent(
+ "<select id='sel' size='2'>"
+ "<option value='1' selected>One</option>"
+ "<option value='2'>Two</option>"
+ "</select>");
+ auto* select = To<HTMLSelectElement>(GetElementById("sel"));
+ select->SetSuggestedValue("2");
+ ASSERT_TRUE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+
+ Element* popover = select->GetAutofillPreviewElement();
+ ASSERT_TRUE(popover);
+
+ const Position start = Position::FirstPositionInNode(*popover);
+ const Position end = Position::LastPositionInNode(*popover);
+
+ // Without ExcludeAutofilledValue, the preview text in the popover is emitted.
+ EXPECT_EQ("[Two]", IteratePartial<DomTree>(start, end));
+
+ // With ExcludeAutofilledValue, the preview text must be suppressed when the
+ // fix is enabled.
+ TextIteratorBehavior behavior =
+ TextIteratorBehavior::Builder().SetExcludeAutofilledValue(true).Build();
+ EXPECT_EQ("", IteratePartial<DomTree>(start, end, behavior));
+
+ // When the feature is disabled, the previous behavior is restored (preview
+ // text is emitted).
+ ScopedTextIteratorExcludeAutofilledSelectFixForTest disable_fix(false);
+ EXPECT_EQ("[Two]", IteratePartial<DomTree>(start, end, behavior));
+}
+
} // namespace text_iterator_test
} // namespace blink
Original Bug Report
Autofill PII Leak via TextIterator ExcludeAutofilledValue Select Omission
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: The ExcludeAutofilledValue guard in TextIteratorAlgorithm::HandleTextNode checks only EnclosingTextControl, which matches input and textarea elements but omits select elements. When an Autofill preview is active, the preview text popover in the select element’s UA shadow root is not suppressed during selection serialization. Page JavaScript can potentially position the DOM selection inside the popover and serialize the previewed option label via getSelection().toString(), leaking Autofill PII.
Affected files:
third_party/blink/renderer/core/editing/iterators/text_iterator.ccthird_party/blink/renderer/core/html/forms/text_control_element.ccthird_party/blink/renderer/core/html/forms/select_type.ccthird_party/blink/renderer/core/editing/finder/find_buffer.cc
Estimated timestamp from git blame: 2026-08-10
Summary
Chromium’s TextIteratorAlgorithm<Strategy>::HandleTextNode contains a security guard intended to prevent sensitive Autofill preview text from being exposed to web content during text extraction and selection serialization (AdjustBehaviorFlags<EditingStrategy> forces ExcludeAutofilledValue(true) for DomSelection::toString()). However, this guard determines whether a text node belongs to a previewed control using EnclosingTextControl(node_), which only inspects elements inheriting from TextControlElement (<input> and <textarea>).
With SelectAutofillPopoverPreview enabled, <select> elements render Autofill previews inside a UA-shadow popover (::-internal-select-autofill-preview). Because HTMLSelectElement does not inherit from TextControlElement, EnclosingTextControl returns nullptr. Consequently, the preview text in the popover is not suppressed and can potentially be serialized into plaintext via getSelection().toString().
Root Cause Analysis
- Guard Implementation (
text_iterator.cc:533-543):
template <typename Strategy>
void TextIteratorAlgorithm<Strategy>::HandleTextNode() {
if (ExcludesAutofilledValue()) {
TextControlElement* control = EnclosingTextControl(node_);
// For security reason, we don't expose suggested value if it is
// auto-filled.
// TODO(crbug.com/1472209): Only hide suggested value of previews.
if (control && (control->IsAutofilled() || control->IsPreviewed())) {
return;
}
}
- Predicate Limitation (
text_control_element.cc:1268-1276):
TextControlElement* EnclosingTextControl(const Node* container) {
if (!container)
return nullptr;
Element* ancestor = container->OwnerShadowHost();
return ancestor && IsTextControl(*ancestor) &&
container->ContainingShadowRoot()->IsUserAgent()
? ToTextControl(ancestor)
: nullptr;
}
IsTextControl (text_control_element.h:435-441) returns true only for TextControlElement instances (HTMLInputElement, HTMLTextAreaElement). HTMLSelectElement derives directly from HTMLFormControlElementWithState, so EnclosingTextControl returns nullptr for nodes within the <select> UA shadow root.
-
Autofill Select Popover (
select_type.cc:2083-2123): When an Autofill preview is shown,SelectType::DidSetSuggestedOptionopensautofill_popover_(HTMLSelectElement::SelectAutofillPreviewElement) and populatesautofill_popover_text_with the previewed option’s label (setInnerText(option->label())). -
Selection Serialization (
dom_selection.cc:875-896):DomSelection::toString()serializes the normalized DOM range without adjusting for shadow boundaries. BecauseEnclosingTextControlreturnsnullptr,HandleTextNodefalls through and appends the preview text to the serialized output.
Potential Attack Scenario
Note: The following steps describe a potential exploitation sequence based on source code analysis. Our tooling environment does not currently have the capability to run live exploit code.
- The page contains
<input autocomplete="name">and a listbox<select size="2" autocomplete="country" id="s">styled with#s option { visibility: hidden; }. - The user focuses the input and hovers over an Autofill suggestion in the browser UI, triggering
PreviewFormFieldin the renderer (form_autofill_util.cc:1392-1432). - The select element enters
kPreviewedstate and renders the previewed option’s label in its UA shadow popover. - Attacker script detects preview onset and clears each option’s value attribute (
for (const o of s.options) o.value = '';). This causesHTMLSelectElement::SuggestedValue()to return""without closing the popover, bypassingFindBuffer::ShouldIgnoreContents(find_buffer.cc:80-86). - Attacker script runs
window.find(c)for characters in the probe alphabet.Editor::FindString(editor.cc:800-822) matches the character in the popover and sets the DOM selection inside the popover’s text node. - Script executes
getSelection().modify('extend', 'forward', 'lineboundary')followed bygetSelection().toString(). BecauseEnclosingTextControlreturnsnullptr, the guard is bypassed and the previewed PII substring is returned in plaintext. - Repeating with backward extension reconstructs the full previewed label string without requiring user confirmation.
Suggested Fix
In TextIteratorAlgorithm<Strategy>::HandleTextNode (third_party/blink/renderer/core/editing/iterators/text_iterator.cc), extend the autofill preview check to also handle HTMLSelectElement and its preview popovers:
- Check if
node_->OwnerShadowHost()is anHTMLSelectElementthat satisfiesselect->IsPreviewed() || select->IsAutofilled(). - Alternatively, generalize the check to inspect any
HTMLFormControlElementWithStatehost that is in a previewed or autofilled state before serializing UA shadow content.
Evaluated with Chrome root at commit: 53c437ac4ea52cc89d0265eb9e6d68666fdb303e
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.