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 <[email protected]> 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 <[email protected]> Reviewed-by: Adem Derinel <[email protected]> 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