Chrome · Autofill › Android
CVE-2026-106249
Authorization Error in Autofill (Android)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java |
modified | |
AndroidAutofillProviderWithCredManTestcomponents/android_autofill/browser/android_autofill_provider_unittest.cc |
modified |
Files Changed
components/android_autofill/browser/android_autofill_features.cccomponents/android_autofill/browser/android_autofill_provider.cccomponents/android_autofill/browser/android_autofill_provider_unittest.cccomponents/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.javacomponents/android_autofill/browser/junit/src/org/chromium/components/autofill/AutofillProviderTest.java
Patch
From 7daefd85906a1692605903713f4fa09b208c9750 Mon Sep 17 00:00:00 2001
From: Friedrich Horschig <friedrichh@chromium.org>
Date: Tue, 08 Sep 2026 06:43:09 -0700
Subject: [PATCH] [Android][Autofill] Verify origin for select updates
Avoid updating the active autofill session's current field or shifting
focus when an unfocused or cross-origin select field changes its value.
Key changes:
- AndroidAutofillProvider::OnSelectControlSelectionChanged: only call UpdateCurrentField if the form is unlinked or the field matches the active session's current field.
- AutofillProvider.onFormFieldDidChange, prevent transferring virtual focus to an unfocused field on value changes.
Screencast Before:
http://go/scrcast/NDYxNTIzNTk4NDk0OTI0OHw1YWMzODVlOC1lNw
Screencast w/ Fix:
http://go/scrcast/NjY3MDc3NDA4MDM3MjczNnw5YjQwNmU0OS1jOA
Feature: AndroidAutofillFieldsUpdatedOnSelect
Fixed: 552440317
Change-Id: Ia3425ce3044e885c8d6750bd8b67b4188552b8a9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8354306
Commit-Queue: Friedrich Hauser <friedrichh@chromium.org>
Reviewed-by: Adem Derinel <derinel@google.com>
Cr-Commit-Position: refs/heads/main@{#1693747}
---
diff --git a/components/android_autofill/browser/android_autofill_features.cc b/components/android_autofill/browser/android_autofill_features.cc
index 15de0ee..881a0a4 100644
--- a/components/android_autofill/browser/android_autofill_features.cc
+++ b/components/android_autofill/browser/android_autofill_features.cc
@@ -42,7 +42,7 @@
base::FEATURE_ENABLED_BY_DEFAULT);
// If enabled, fields are updated whenever a user interacts with a <select>.
-// TODO(crbug.com/502346855): Remove in M152 or later.
+// TODO(crbug.com/502346855): Remove in M157 or later.
BASE_FEATURE(kAndroidAutofillFieldsUpdatedOnSelect,
base::FEATURE_ENABLED_BY_DEFAULT);
diff --git a/components/android_autofill/browser/android_autofill_provider.cc b/components/android_autofill/browser/android_autofill_provider.cc
index 7088548..c9b4868 100644
--- a/components/android_autofill/browser/android_autofill_provider.cc
+++ b/components/android_autofill/browser/android_autofill_provider.cc
@@ -531,7 +531,11 @@
}
if (base::FeatureList::IsEnabled(
features::kAndroidAutofillFieldsUpdatedOnSelect)) {
- UpdateCurrentField(manager, form, field);
+ if (!IsLinkedForm(form) ||
+ (session_state_ &&
+ session_state_->current_field.id == field.global_id())) {
+ UpdateCurrentField(manager, form, field);
+ }
}
if (!IsLinkedForm(form)) {
StartNewSession(manager, form, field);
diff --git a/components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java b/components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java
index 1fbf25b..7352588 100644
--- a/components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java
+++ b/components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java
@@ -398,7 +398,11 @@
short shortIndex = (short) index;
FocusField focusField = mRequest.getFocusField();
if (focusField == null || shortIndex != focusField.fieldIndex) {
- onFocusChangedImpl(true, index, x, y, width, height, /* causedByValueChange= */ true);
+ if (!AndroidAutofillFeatures.ANDROID_AUTOFILL_FIELDS_UPDATED_ON_SELECT.isEnabled()
+ || focusField == null) {
+ onFocusChangedImpl(
+ true, index, x, y, width, height, /* causedByValueChange= */ true);
+ }
} else {
// Currently there is no api to notify both value and position
// change, before the API is available, we still need to call
diff --git a/components/android_autofill/browser/junit/src/org/chromium/components/autofill/AutofillProviderTest.java b/components/android_autofill/browser/junit/src/org/chromium/components/autofill/AutofillProviderTest.java
index 16ae698a..489c6990 100644
--- a/components/android_autofill/browser/junit/src/org/chromium/components/autofill/AutofillProviderTest.java
+++ b/components/android_autofill/browser/junit/src/org/chromium/components/autofill/AutofillProviderTest.java
@@ -635,6 +635,133 @@
verify(mNativeMock, never()).hasPasskeyRequest(anyLong());
}
+ @Test
+ @Features.EnableFeatures({
+ AndroidAutofillFeatures.ANDROID_AUTOFILL_FIELDS_UPDATED_ON_SELECT_NAME
+ })
+ public void testOnFormFieldDidChange_unfocusedFieldChanged_featureEnabled() {
+ int sessionId = 123;
+ FormFieldDataBuilder field1Builder = new FormFieldDataBuilder();
+ FormFieldDataBuilder field2Builder = new FormFieldDataBuilder();
+ FormData formData =
+ new FormData(
+ sessionId,
+ /* name= */ null,
+ /* host= */ null,
+ Arrays.asList(field1Builder.build(), field2Builder.build()));
+
+ int field0VirtualId = FormData.toFieldVirtualId(sessionId, (short) 0);
+ int field1VirtualId = FormData.toFieldVirtualId(sessionId, (short) 1);
+
+ // Start session focused on field 0.
+ mAutofillProvider.startAutofillSession(
+ formData,
+ /* focus= */ 0,
+ /* x= */ 0,
+ /* y= */ 0,
+ /* width= */ 50,
+ /* height= */ 20,
+ /* hasServerPrediction= */ false);
+
+ assertEquals(field0VirtualId, mFocusVirtualId);
+
+ // Mutate unfocused field 1.
+ mAutofillProvider.onFormFieldDidChange(
+ /* index= */ 1, /* x= */ 0, /* y= */ 0, /* width= */ 50, /* height= */ 20);
+
+ // Focus should remain on field 0 and not shift to field 1.
+ assertEquals(field0VirtualId, mFocusVirtualId);
+ verify(mAutofillManager, never()).notifyViewExited(eq(mContainerView), eq(field0VirtualId));
+ verify(mAutofillManager, never())
+ .notifyViewEntered(eq(mContainerView), eq(field1VirtualId), any());
+ }
+
+ @Test
+ @Features.DisableFeatures({
+ AndroidAutofillFeatures.ANDROID_AUTOFILL_FIELDS_UPDATED_ON_SELECT_NAME
+ })
+ public void testOnFormFieldDidChange_unfocusedFieldChanged_featureDisabled() {
+ int sessionId = 123;
+ FormFieldDataBuilder field1Builder = new FormFieldDataBuilder();
+ FormFieldDataBuilder field2Builder = new FormFieldDataBuilder();
+ FormData formData =
+ new FormData(
+ sessionId,
+ /* name= */ null,
+ /* host= */ null,
+ Arrays.asList(field1Builder.build(), field2Builder.build()));
+
+ int field0VirtualId = FormData.toFieldVirtualId(sessionId, (short) 0);
+ int field1VirtualId = FormData.toFieldVirtualId(sessionId, (short) 1);
+
+ // Start session focused on field 0.
+ mAutofillProvider.startAutofillSession(
+ formData,
+ /* focus= */ 0,
+ /* x= */ 0,
+ /* y= */ 0,
+ /* width= */ 50,
+ /* height= */ 20,
+ /* hasServerPrediction= */ false);
+
+ assertEquals(field0VirtualId, mFocusVirtualId);
+
+ // Mutate unfocused field 1.
+ mAutofillProvider.onFormFieldDidChange(
+ /* index= */ 1, /* x= */ 0, /* y= */ 0, /* width= */ 50, /* height= */ 20);
+
+ // With the feature disabled (killswitch), focus shifts to field 1.
+ assertEquals(field1VirtualId, mFocusVirtualId);
+ verify(mAutofillManager).notifyViewExited(eq(mContainerView), eq(field0VirtualId));
+ verify(mAutofillManager).notifyViewEntered(eq(mContainerView), eq(field1VirtualId), any());
+ }
+
+ @Test
+ @Features.EnableFeatures({
+ AndroidAutofillFeatures.ANDROID_AUTOFILL_FIELDS_UPDATED_ON_SELECT_NAME
+ })
+ public void testOnFormFieldDidChange_noFocusedField_featureEnabled() {
+ int sessionId = 123;
+ FormFieldDataBuilder field1Builder = new FormFieldDataBuilder();
+ FormFieldDataBuilder field2Builder = new FormFieldDataBuilder();
+ FormData formData =
+ new FormData(
+ sessionId,
+ /* name= */ null,
+ /* host= */ null,
+ Arrays.asList(field1Builder.build(), field2Builder.build()));
+
+ int field0VirtualId = FormData.toFieldVirtualId(sessionId, (short) 0);
+
+ // Start session focused on field 0.
+ mAutofillProvider.startAutofillSession(
+ formData,
+ /* focus= */ 0,
+ /* x= */ 0,
+ /* y= */ 0,
+ /* width= */ 50,
+ /* height= */ 20,
+ /* hasServerPrediction= */ false);
+
+ // Clear focus.
+ mAutofillProvider.onFocusChanged(
+ /* focusOnForm= */ false,
+ /* focusField= */ 0,
+ /* x= */ 0,
+ /* y= */ 0,
+ /* width= */ 0,
+ /* height= */ 0);
+
+ // Value changes on field 0 when no field is currently focused.
+ mAutofillProvider.onFormFieldDidChange(
+ /* index= */ 0, /* x= */ 0, /* y= */ 0, /* width= */ 50, /* height= */ 20);
+
+ // Focus should be gained on field 0.
+ assertEquals(field0VirtualId, mFocusVirtualId);
+ verify(mAutofillManager, Mockito.atLeastOnce())
+ .notifyViewEntered(eq(mContainerView), eq(field0VirtualId), any());
+ }
+
FormData setupPrefillRequest(int sessionId) {
FormFieldDataBuilder field1Builder = new FormFieldDataBuilder();
field1Builder.mBounds =
diff --git a/components/android_autofill/browser/android_autofill_provider_unittest.cc b/components/android_autofill/browser/android_autofill_provider_unittest.cc
index ccf6806..26e8b02 100644
--- a/components/android_autofill/browser/android_autofill_provider_unittest.cc
+++ b/components/android_autofill/browser/android_autofill_provider_unittest.cc
@@ -802,19 +802,42 @@
android_autofill_manager().OnFormsSeen({form}, /*removed_forms=*/{},
AutofillManagerTestApi::pass_key());
- // Start an Autofill session.
+ FormFieldData field = form.fields()[0];
+ url::Origin select_origin = url::Origin::Create(GURL("https://bar.com"));
+ field.set_origin(select_origin);
+
+ // Simulate OnSelectControlSelectionChanged on an unlinked form.
+ autofill_provider().OnSelectControlSelectionChanged(
+ &android_autofill_manager(), form, field);
+
+ EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ select_origin);
+}
+
+// Tests that OnSelectControlSelectionChanged updates the origin when the
+// selection change occurs on the currently focused field.
+TEST_F(AndroidAutofillProviderTest,
+ OnSelectControlSelectionChangedUpdatesOriginForFocusedField) {
+ base::test::ScopedFeatureList scoped_feature_list{
+ features::kAndroidAutofillFieldsUpdatedOnSelect};
+
+ FormData form = CreateFormDataForFrame(
+ CreateTestPersonalInformationFormData(), main_frame_token());
+ android_autofill_manager().OnFormsSeen({form}, /*removed_forms=*/{},
+ AutofillManagerTestApi::pass_key());
+
+ // Start an Autofill session on field 0.
android_autofill_manager().OnAskForValuesToFillTest(
form, form.fields()[0].global_id());
EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
form.fields()[0].origin());
- // Create a new field with a different origin.
- FormFieldData field = form.fields()[1];
+ // Update selection on the same focused field.
+ FormFieldData field = form.fields()[0];
url::Origin new_origin = url::Origin::Create(GURL("https://bar.com"));
field.set_origin(new_origin);
- // Simulate OnSelectControlSelectionChanged.
autofill_provider().OnSelectControlSelectionChanged(
&android_autofill_manager(), form, field);
@@ -822,6 +845,80 @@
new_origin);
}
+// Tests that an unfocused select control's value change in a multi-origin form
+// does not overwrite the active session's focused field origin (b/552440317).
+TEST_F(AndroidAutofillProviderTest,
+ UnfocusedSelectDoesNotOverwriteFocusedFieldOrigin) {
+ base::test::ScopedFeatureList scoped_feature_list{
+ features::kAndroidAutofillFieldsUpdatedOnSelect};
+
+ FormData form = CreateFormDataForFrame(
+ CreateTestPersonalInformationFormData(), main_frame_token());
+ url::Origin iframe_origin =
+ url::Origin::Create(GURL("https://iframe-payment.example"));
+ url::Origin parent_origin =
+ url::Origin::Create(GURL("https://parent-attacker.example"));
+
+ test_api(form).field(0).set_origin(iframe_origin);
+ test_api(form).field(1).set_origin(parent_origin);
+
+ android_autofill_manager().OnFormsSeen({form}, /*removed_forms=*/{},
+ AutofillManagerTestApi::pass_key());
+
+ // 1. User taps and focuses field 0 inside the iframe.
+ android_autofill_manager().OnAskForValuesToFillTest(
+ form, form.fields()[0].global_id());
+
+ ASSERT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ iframe_origin);
+
+ // 2. Parent page mutates unfocused select field 1.
+ autofill_provider().OnSelectControlSelectionChanged(
+ &android_autofill_manager(), form, form.fields()[1]);
+
+ // The active session origin must remain iframe_origin.
+ EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ iframe_origin);
+}
+
+// Tests that when kAndroidAutofillFieldsUpdatedOnSelect is disabled
+// (killswitch), an unfocused select control does NOT overwrite the active
+// session origin.
+TEST_F(AndroidAutofillProviderTest,
+ UnfocusedSelectWithFeatureDisabledPreservesFocusedFieldOrigin) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndDisableFeature(
+ features::kAndroidAutofillFieldsUpdatedOnSelect);
+
+ FormData form = CreateFormDataForFrame(
+ CreateTestPersonalInformationFormData(), main_frame_token());
+ url::Origin iframe_origin =
+ url::Origin::Create(GURL("https://iframe-payment.example"));
+ url::Origin parent_origin =
+ url::Origin::Create(GURL("https://parent-attacker.example"));
+
+ test_api(form).field(0).set_origin(iframe_origin);
+ test_api(form).field(1).set_origin(parent_origin);
+
+ android_autofill_manager().OnFormsSeen({form}, /*removed_forms=*/{},
+ AutofillManagerTestApi::pass_key());
+
+ // 1. User taps and focuses field 0 inside the iframe.
+ android_autofill_manager().OnAskForValuesToFillTest(
+ form, form.fields()[0].global_id());
+
+ ASSERT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ iframe_origin);
+
+ // 2. Parent page mutates unfocused select field 1.
+ autofill_provider().OnSelectControlSelectionChanged(
+ &android_autofill_manager(), form, form.fields()[1]);
+
+ // With the feature disabled, the active session origin is preserved.
+ EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ iframe_origin);
+}
+
class AndroidAutofillProviderWithCredManTest
: public AndroidAutofillProviderTestBase {
public:
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/components/android_autofill/browser/android_autofill_provider_unittest.cc b/components/android_autofill/browser/android_autofill_provider_unittest.cc
index ccf6806..26e8b02 100644
--- a/components/android_autofill/browser/android_autofill_provider_unittest.cc
+++ b/components/android_autofill/browser/android_autofill_provider_unittest.cc
@@ -802,19 +802,42 @@
android_autofill_manager().OnFormsSeen({form}, /*removed_forms=*/{},
AutofillManagerTestApi::pass_key());
- // Start an Autofill session.
+ FormFieldData field = form.fields()[0];
+ url::Origin select_origin = url::Origin::Create(GURL("https://bar.com"));
+ field.set_origin(select_origin);
+
+ // Simulate OnSelectControlSelectionChanged on an unlinked form.
+ autofill_provider().OnSelectControlSelectionChanged(
+ &android_autofill_manager(), form, field);
+
+ EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ select_origin);
+}
+
+// Tests that OnSelectControlSelectionChanged updates the origin when the
+// selection change occurs on the currently focused field.
+TEST_F(AndroidAutofillProviderTest,
+ OnSelectControlSelectionChangedUpdatesOriginForFocusedField) {
+ base::test::ScopedFeatureList scoped_feature_list{
+ features::kAndroidAutofillFieldsUpdatedOnSelect};
+
+ FormData form = CreateFormDataForFrame(
+ CreateTestPersonalInformationFormData(), main_frame_token());
+ android_autofill_manager().OnFormsSeen({form}, /*removed_forms=*/{},
+ AutofillManagerTestApi::pass_key());
+
+ // Start an Autofill session on field 0.
android_autofill_manager().OnAskForValuesToFillTest(
form, form.fields()[0].global_id());
EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
form.fields()[0].origin());
- // Create a new field with a different origin.
- FormFieldData field = form.fields()[1];
+ // Update selection on the same focused field.
+ FormFieldData field = form.fields()[0];
url::Origin new_origin = url::Origin::Create(GURL("https://bar.com"));
field.set_origin(new_origin);
- // Simulate OnSelectControlSelectionChanged.
autofill_provider().OnSelectControlSelectionChanged(
&android_autofill_manager(), form, field);
@@ -822,6 +845,80 @@
new_origin);
}
+// Tests that an unfocused select control's value change in a multi-origin form
+// does not overwrite the active session's focused field origin (b/552440317).
+TEST_F(AndroidAutofillProviderTest,
+ UnfocusedSelectDoesNotOverwriteFocusedFieldOrigin) {
+ base::test::ScopedFeatureList scoped_feature_list{
+ features::kAndroidAutofillFieldsUpdatedOnSelect};
+
+ FormData form = CreateFormDataForFrame(
+ CreateTestPersonalInformationFormData(), main_frame_token());
+ url::Origin iframe_origin =
+ url::Origin::Create(GURL("https://iframe-payment.example"));
+ url::Origin parent_origin =
+ url::Origin::Create(GURL("https://parent-attacker.example"));
+
+ test_api(form).field(0).set_origin(iframe_origin);
+ test_api(form).field(1).set_origin(parent_origin);
+
+ android_autofill_manager().OnFormsSeen({form}, /*removed_forms=*/{},
+ AutofillManagerTestApi::pass_key());
+
+ // 1. User taps and focuses field 0 inside the iframe.
+ android_autofill_manager().OnAskForValuesToFillTest(
+ form, form.fields()[0].global_id());
+
+ ASSERT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ iframe_origin);
+
+ // 2. Parent page mutates unfocused select field 1.
+ autofill_provider().OnSelectControlSelectionChanged(
+ &android_autofill_manager(), form, form.fields()[1]);
+
+ // The active session origin must remain iframe_origin.
+ EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ iframe_origin);
+}
+
+// Tests that when kAndroidAutofillFieldsUpdatedOnSelect is disabled
+// (killswitch), an unfocused select control does NOT overwrite the active
+// session origin.
+TEST_F(AndroidAutofillProviderTest,
+ UnfocusedSelectWithFeatureDisabledPreservesFocusedFieldOrigin) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndDisableFeature(
+ features::kAndroidAutofillFieldsUpdatedOnSelect);
+
+ FormData form = CreateFormDataForFrame(
+ CreateTestPersonalInformationFormData(), main_frame_token());
+ url::Origin iframe_origin =
+ url::Origin::Create(GURL("https://iframe-payment.example"));
+ url::Origin parent_origin =
+ url::Origin::Create(GURL("https://parent-attacker.example"));
+
+ test_api(form).field(0).set_origin(iframe_origin);
+ test_api(form).field(1).set_origin(parent_origin);
+
+ android_autofill_manager().OnFormsSeen({form}, /*removed_forms=*/{},
+ AutofillManagerTestApi::pass_key());
+
+ // 1. User taps and focuses field 0 inside the iframe.
+ android_autofill_manager().OnAskForValuesToFillTest(
+ form, form.fields()[0].global_id());
+
+ ASSERT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ iframe_origin);
+
+ // 2. Parent page mutates unfocused select field 1.
+ autofill_provider().OnSelectControlSelectionChanged(
+ &android_autofill_manager(), form, form.fields()[1]);
+
+ // With the feature disabled, the active session origin is preserved.
+ EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ iframe_origin);
+}
+
class AndroidAutofillProviderWithCredManTest
: public AndroidAutofillProviderTestBase {
public:
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