CVE-2026-106308
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/android_autofill/browser/android_autofill_provider.cc |
modified |
Files Changed
components/android_autofill/browser/android_autofill_provider.cccomponents/android_autofill/browser/android_autofill_provider_unittest.cc
Patch
From 5261541a3ec9236389e677a45351fe8549184887 Mon Sep 17 00:00:00 2001
From: Jihad Hanna <jihadghanna@google.com>
Date: Sun, 13 Sep 2026 23:52:28 -0700
Subject: [PATCH] Enforce no field addition/removal in IsFormSimilarToCachedForm()
Since Java uses indices only to identify forms for the time being, we
need to make sure when using a form that could have mutated, that the
field structure is still the same.
Fixed: 560055258
Change-Id: I5c9e8d226374f15b750ea5758b6d7370dc576f06
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8398648
Commit-Queue: Jan Keitel <jkeitel@google.com>
Auto-Submit: Jihad Hanna <jihadghanna@google.com>
Reviewed-by: Jan Keitel <jkeitel@google.com>
Cr-Commit-Position: refs/heads/main@{#1697067}
---
diff --git a/components/android_autofill/browser/android_autofill_provider.cc b/components/android_autofill/browser/android_autofill_provider.cc
index c9b4868..7700f5b 100644
--- a/components/android_autofill/browser/android_autofill_provider.cc
+++ b/components/android_autofill/browser/android_autofill_provider.cc
@@ -266,6 +266,11 @@
if (!cached_data_ || !cached_data_->cached_form) {
return false;
}
+ if (!std::ranges::equal(cached_data_->cached_form->form().fields(),
+ form.fields(), {}, &FormFieldData::global_id,
+ &FormFieldData::global_id)) {
+ return false;
+ }
if (form_structure) {
CHECK_EQ(form.global_id(), form_structure->global_id());
std::unique_ptr<PasswordForm> pw_form =
diff --git a/components/android_autofill/browser/android_autofill_provider_unittest.cc b/components/android_autofill/browser/android_autofill_provider_unittest.cc
index 26e8b02..a9746ed5 100644
--- a/components/android_autofill/browser/android_autofill_provider_unittest.cc
+++ b/components/android_autofill/browser/android_autofill_provider_unittest.cc
@@ -1842,6 +1842,60 @@
PrefillRequestState::kRequestSentFormChanged, 1);
}
+// Tests that the session id used in a prefill request is not reused when
+// the form structure changes by inserting fields before the cached login
+// fields, even if the login fields themselves are still present in the form.
+TEST_F(AndroidAutofillProviderPrefillRequestTest,
+ SessionIdIsNotReusedWhenFieldsAreInserted) {
+ if (base::android::android_info::sdk_int() <
+ base::android::android_info::SDK_VERSION_U) {
+ GTEST_SKIP();
+ }
+
+ base::HistogramTester histogram_tester;
+ FormData form =
+ CreateFormDataForFrame(CreateTestLoginForm(), main_frame_token());
+ android_autofill_manager().OnFormsSeen({form}, /*removed_forms=*/{},
+ AutofillManagerTestApi::pass_key());
+
+ // Upon receiving server predictions a prefill request should be sent.
+ SessionId cache_session_id = SessionId(0);
+ EXPECT_CALL(provider_bridge(), SendPrefillRequest(EqualsFormData(form)))
+ .WillOnce(SaveSessionId(&cache_session_id));
+ android_autofill_manager().SimulatePropagateAutofillPredictions(
+ form.global_id());
+ Mock::VerifyAndClearExpectations(&provider_bridge());
+
+ // Insert a new field at the beginning of the form (e.g. from an iframe or
+ // dynamic DOM insertion).
+ FormData changed_form = form;
+ FormFieldData new_field = CreateTestFormField(
+ /*label=*/"Injected", /*name=*/"injected", /*value=*/"",
+ FormControlType::kInputText);
+ new_field.set_host_frame(main_frame_token());
+ test_api(changed_form).Insert(0, new_field);
+
+ android_autofill_manager().OnFormsSeen({changed_form},
+ /*removed_forms=*/{},
+ AutofillManagerTestApi::pass_key());
+ SessionId autofill_session_id = SessionId(0);
+ EXPECT_CALL(provider_bridge(),
+ StartAutofillSession(EqualsFormData(changed_form),
+ EqualsFieldInfo(/*index=*/0),
+ /*has_server_predictions=*/true))
+ .WillOnce(WithArg<0>(SaveSessionId(&autofill_session_id)));
+ android_autofill_manager().OnAskForValuesToFillTest(
+ changed_form, changed_form.fields().front().global_id());
+ Mock::VerifyAndClearExpectations(&provider_bridge());
+
+ // A new session id must be used because field indices no longer match the
+ // prefill request.
+ EXPECT_NE(cache_session_id, autofill_session_id);
+ histogram_tester.ExpectUniqueSample(
+ AndroidAutofillProvider::kPrefillRequestStateUma,
+ PrefillRequestState::kRequestSentFormChanged, 1);
+}
+
// Tests that the session id used in a prefill request is only used once to
// start an Autofill session. If the user then focuses on a different form
// before returning to the (formerly) cached form, a new session is started.
Regression Test / PoC
diff --git a/components/android_autofill/browser/android_autofill_provider_unittest.cc b/components/android_autofill/browser/android_autofill_provider_unittest.cc
index 26e8b02..a9746ed5 100644
--- a/components/android_autofill/browser/android_autofill_provider_unittest.cc
+++ b/components/android_autofill/browser/android_autofill_provider_unittest.cc
@@ -1842,6 +1842,60 @@
PrefillRequestState::kRequestSentFormChanged, 1);
}
+// Tests that the session id used in a prefill request is not reused when
+// the form structure changes by inserting fields before the cached login
+// fields, even if the login fields themselves are still present in the form.
+TEST_F(AndroidAutofillProviderPrefillRequestTest,
+ SessionIdIsNotReusedWhenFieldsAreInserted) {
+ if (base::android::android_info::sdk_int() <
+ base::android::android_info::SDK_VERSION_U) {
+ GTEST_SKIP();
+ }
+
+ base::HistogramTester histogram_tester;
+ FormData form =
+ CreateFormDataForFrame(CreateTestLoginForm(), main_frame_token());
+ android_autofill_manager().OnFormsSeen({form}, /*removed_forms=*/{},
+ AutofillManagerTestApi::pass_key());
+
+ // Upon receiving server predictions a prefill request should be sent.
+ SessionId cache_session_id = SessionId(0);
+ EXPECT_CALL(provider_bridge(), SendPrefillRequest(EqualsFormData(form)))
+ .WillOnce(SaveSessionId(&cache_session_id));
+ android_autofill_manager().SimulatePropagateAutofillPredictions(
+ form.global_id());
+ Mock::VerifyAndClearExpectations(&provider_bridge());
+
+ // Insert a new field at the beginning of the form (e.g. from an iframe or
+ // dynamic DOM insertion).
+ FormData changed_form = form;
+ FormFieldData new_field = CreateTestFormField(
+ /*label=*/"Injected", /*name=*/"injected", /*value=*/"",
+ FormControlType::kInputText);
+ new_field.set_host_frame(main_frame_token());
+ test_api(changed_form).Insert(0, new_field);
+
+ android_autofill_manager().OnFormsSeen({changed_form},
+ /*removed_forms=*/{},
+ AutofillManagerTestApi::pass_key());
+ SessionId autofill_session_id = SessionId(0);
+ EXPECT_CALL(provider_bridge(),
+ StartAutofillSession(EqualsFormData(changed_form),
+ EqualsFieldInfo(/*index=*/0),
+ /*has_server_predictions=*/true))
+ .WillOnce(WithArg<0>(SaveSessionId(&autofill_session_id)));
+ android_autofill_manager().OnAskForValuesToFillTest(
+ changed_form, changed_form.fields().front().global_id());
+ Mock::VerifyAndClearExpectations(&provider_bridge());
+
+ // A new session id must be used because field indices no longer match the
+ // prefill request.
+ EXPECT_NE(cache_session_id, autofill_session_id);
+ histogram_tester.ExpectUniqueSample(
+ AndroidAutofillProvider::kPrefillRequestStateUma,
+ PrefillRequestState::kRequestSentFormChanged, 1);
+}
+
// Tests that the session id used in a prefill request is only used once to
// start an Autofill session. If the user then focuses on a different form
// before returning to the (formerly) cached form, a new session is started.
Original Bug Report
Android Autofill prefill session reuse maps authenticated victim credentials into cross-origin iframe fields
Report description
Android Autofill prefill session reuse maps authenticated victim credentials into cross-origin iframe fields
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
The problem
Please describe the technical details of the vulnerability
Summary
Chrome Android reuses a cached platform-Autofill prefill session after a
frame-transcending login form changes field order. The similarity decision
compares only the password parser’s selected username/password
FieldGlobalIds; Android virtual IDs encode only the reused Chrome session and
the field’s numeric position. If a cross-origin child frame inserts two fields
before the unchanged victim login fields after prefill, authenticated values
returned for cached positions 0 and 1 are resolved against the new positions
and filled into the attacker frame.
Official Google Chrome Stable 152.0.7977.75 reproduced the full ordinary-web path on Android 15.
Bisect
- Range: 1249369 → 1249370 — https://chromium.googlesource.com/chromium/src/+log/152cc8489821e33424b31439e17f95c0ab57693a..b7240deab66b4bb9170fdcfe72ccbbed8509f983
- Introduced by: https://chromium.googlesource.com/chromium/src/+/b7240deab66b4bb9170fdcfe72ccbbed8509f983 — “Use PWM::FormDataParser predictions to perform similarity check.”
- Landed on: M122.0.6259.0
The introducing diff replaced whole-form similarity with a decision that the
two forms are similar when the parser-selected username/password fields are
identical. Commit 50840faf96313b10a76eafb23390833dab166fe0 later removed
the experiment guard after that branch had been used in production.
Affected versions and validation
- Independent runtime: official Google Chrome Stable 152.0.7977.75
(
com.android.chrome, versionCode 797707504), signed by Google’s Android release certificate, on Android 15 / API 35 build AE3A.240806.019. - Runtime-matched Stable source: Chromium 152.0.7977.75, revision
8733d5f725f6d261c6023b73b466e81e84fda941, contains the complete chain. - Current Android Stable source: Chrome 154.0.8037.21, revision
0fa6d91e2faf313da8332688a96bd98b9d983a4d, still contains the same parser-override comparison, cached session-ID reuse, and positional lookup. - Current main source: Chromium 155.0.8053.0, revision
0a4b6ddf05517df35bc87e279bce4d4c9a545e9a, also contains the complete vulnerable chain. - Platforms: Chrome Android using the user-enabled Android platform Autofill service on Android 14 or Android 15. Chrome’s built-in password manager and Android 16 are not claimed.
The end-to-end validation used a disposable Android profile, normal public HTTPS, the supported Chrome setting Autofill using another service.
Security boundary and prerequisites
The protected asset is the credential dataset selected for a victim-origin
login form. The attacker controls ordinary JavaScript in a cross-origin iframe
embedded before the victim’s username/password fields. The victim page has
delegated the autofill permission to that child frame, but that delegation
does not authorize delivery of the victim form’s credential dataset into the
child origin.
The prerequisites are:
- the top-level login form contains a cross-origin iframe before its credential
controls and delegates
autofillto the frame; - Chrome receives normal login-form predictions and prefetches the Android platform-Autofill response;
- the attacker inserts two non-login text fields only after that prefill;
- the user focuses the first attacker field and selects the cached Autofill suggestion; and
- a selected Android Autofill provider returns the dataset after its normal authentication step.
The supplied provider is an ordinary Android AutofillService. It does not
patch, drive, or substitute for Chrome, and nothing in the chain depends on how
it is implemented: it receives the standard prefill request, returns a standard
Dataset, and the platform applies that dataset by virtual id. Any conforming
third-party provider reaches the same sink, because the defect is Chrome’s reuse
of a cached prefill session across a changed field vector rather than any
provider behaviour. A dedicated provider is used only so the filled value is a
fresh synthetic UUID rather than a real credential.
The supplied provider returns that UUID only after an explicit authentication button, so no real credential is placed at risk. The claim is limited to confidentiality: no account action, credential modification, integrity loss, or availability impact is claimed.
Source chain / root cause
All line-numbered citations below were verified against unmodified files in
Chromium revision 34a482b3cd98d6d599f43091babae366fb9c3d8e. The same named
constructs were separately verified in current Stable revision
0fa6d91e2faf313da8332688a96bd98b9d983a4d and current main revision
0a4b6ddf05517df35bc87e279bce4d4c9a545e9a.
-
components/autofill/core/browser/foundations/form_forest.h:79-87defines the attacker-controllable positional order of a frame-transcending form:> “fields are stored as children of their root forms. The fields are > ordered according to pre-order depth-first (DOM order) traversal”
-
components/android_autofill/browser/android_autofill_provider.cc:263-281treats the changed form as similar by comparing only parser-selected credential identities:PasswordParserOverrides overrides = PasswordParserOverrides::FromLoginForm(*pw_form, *form_structure) .value_or(PasswordParserOverrides()); return cached_data_->password_parser_overrides == overrides;components/android_autofill/browser/android_autofill_provider.cc:292-310then assigns the changed current form the cached session ID:const bool use_cached_form = is_similar_to_cached_form && !has_used_cached_form_; session_state_->form = std::make_unique<FormDataAndroid>( form, use_cached_form ? cached_form->session_id() : CreateSessionId()); -
components/android_autofill/browser/java/src/org/chromium/components/autofill/FormData.java:61-79assigns virtual children in current vector order, andFormData.java:148-150encodes only session and position:int virtualId = toFieldVirtualId(mSessionId, fieldIndex++); child.setAutofillId(assumeNonNull(structure.getAutofillId()), virtualId); ... return (sessionId << 16) | index;The attached runtime log shows this arithmetic directly. The prefill request carried
id=0:i65536andid=0:i65537, which are exactlytoFieldVirtualId(1, 0)andtoFieldVirtualId(1, 1): session 1, positions 0 and 1. The dataset is therefore addressed to positions, not to the fields it was built from, and nothing in the cached identifiers records which document owned position 0 at prefill time. -
components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java:366-381permits the cached bottom sheet when only the cached/current session IDs match:if (mPrefillRequest == null || mPrefillRequest.getForm().mSessionId != mRequest.getForm().mSessionId) { return false; } boolean bottomSheetShown = showAutofillDialog(mContainerView, focus); -
components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillRequest.java:68-92accepts the returned numeric index and writes the value into the current field at that position;AutofillRequest.java:148-153extracts only the packed session and index:if (toSessionId(id) != mFormData.mSessionId) continue; short index = toIndex(id); FormFieldData field = mFormData.mFields.get(index); field.setAutofillValue((String) value.getTextValue()); -
components/android_autofill/browser/android_autofill_provider.cc:401-412reads the Java-backed current form, andcomponents/android_autofill/browser/android_autofill_manager.cc:237-263forwards its filled current fields. Finally,components/autofill/core/browser/foundations/autofill_driver_router.cc:610-628routes those already-remapped fields to their owning frame:if (IsSafeToFill(field, field_type_map.at(field.global_id()), main_origin, triggered_origin)) { fields_of_driver[target].emplace_back(field); }
The downstream origin check therefore sees current attacker fields and the attacker trigger origin. It cannot recover that the returned values were authorized for cached victim fields at the same numeric positions.
Steps to reproduce
-
Publish
poc_victim.htmlandpoc_attacker.htmlon the two HTTPS origins specified inrepro.txt, or use the already deployed URLs. Use Android 14 or Android 15 with a disposable profile in official Chrome Stable. -
Install
poc_autofill_witness.apk, select VRP Autofill Witness in Android’s Autofill settings, then select Autofill using another service in Chrome and accept Chrome’s requested restart. -
Start the provider-only log command from
repro.txt. Open the exact victim URL and do not focus any field until the provider prints:PREFILL_CAPTURED flags=80 ids=2The preceding lines identify position 0 as victim
loginand position 1 as victimpassword, both undervictim.example. -
Click Insert attacker fields after PREFILL_CAPTURED inside the framed attacker page. Focus Attacker B0 and select Authenticate fresh victim credential from the Android Autofill suggestion.
-
Press AUTHENTICATE AND RELEASE FRESH CREDENTIAL. Compare the complete
AUTHENTICATED_FRESH_VALUElog value with the redATTACKER_READBACKblock. Confirm that both victim fields remain empty. -
For the negative control, reopen the exact victim URL and wait for a new prefill marker. Do not insert attacker fields. Focus the victim username, select the dataset, and authenticate. The value fills the victim fields and the attacker frame remains
ATTACKER_EMPTY. -
Run the cleanup commands in
repro.txtto destroy Autofill sessions, uninstall the synthetic provider, and clear the disposable Chrome profile.
Actual result
The prefill contained only the victim-origin fields:
field[0] ... domain=victim.example htmlType=text htmlName=login
field[1] ... domain=victim.example htmlType=password htmlName=password
PREFILL_CAPTURED flags=80 ids=2
After attacker fields were inserted and B0 was focused, authentication generated this fresh value:
AUTHENTICATED_FRESH_VALUE=VRP_PREFILL_FRESH_33eb6022-adb1-4e56-9b23-37be61bdf3a5
Attacker JavaScript read the same value from both attacker-origin fields:
ATTACKER_READBACK
b0=VRP_PREFILL_FRESH_33eb6022-adb1-4e56-9b23-37be61bdf3a5
b1=VRP_PREFILL_FRESH_33eb6022-adb1-4e56-9b23-37be61bdf3a5
The victim username and password fields remained empty. In the negative
control, the authenticated fresh value filled the victim fields and the
attacker frame remained ATTACKER_EMPTY.
Expected result
Chrome should invalidate the cached prefill session whenever the current flattened field vector no longer has the same field identities, order, and origins as the prefilled vector. A dataset authorized for the victim login fields must never be resolved against newly inserted cross-origin fields that occupy the old numeric positions.
Suggested fix
Require whole-form structural similarity in addition to matching
password-parser overrides before reusing the cached prefill session. At minimum,
field count, order, FieldGlobalId, and origin at every virtual position must
remain stable; otherwise Chrome should discard the cached response and create a
new local session ID. A stronger defense would bind each Android virtual ID to
a stable field identity or form-version digest rather than to a reusable
numeric index alone.
fix_require_structural_form_similarity.patch implements the minimal guard by
requiring FormDataAndroid::SimilarFormAs(form) before the parser-override
comparison. It applies cleanly to the cited current source. The touched code is
browser-process Android Autofill code; no renderer or multi-process PoC code is
added.
Attachments
| Name | Description | SHA-256 |
|---|---|---|
poc_victim.html |
Static victim-origin login page that embeds the attacker origin before its credential fields. | 9f84164febe85b16661ee692392236ad2bfc00b86e08d2f32727469a8b941a5e |
poc_attacker.html |
Static attacker-origin iframe that inserts two fields after the prefill marker and displays JavaScript readback. | 96dfe0715aba51ce2a44f57a49b81f120db4391156219155a020f45b821d1d74 |
poc_autofill_witness.apk |
Signed Android Autofill service 2.2 that records cached victim field IDs and releases a fresh synthetic UUID only after explicit authentication. | cbffbfab774ac61e98a9d563e5b3762f1df742c8003038e44e4ea5d98e9d5f6f |
repro.txt |
Standalone public-HTTPS deployment, configuration, vulnerable-flow, negative-control, and cleanup instructions. | bc7a545bc28c810742a5cc5de37e3189b50b47518151a9f0c704c7b4d2e54461 |
evidence_android15_runtime_log.txt |
Sanitized stock-product metadata, prefill field identities, fresh authenticated values, vulnerable result, and negative control. | 528e690f27e6bf31e11a0fea1f086b0d91477fb46e8b3d8bc8a9244a2f16cf0d |
evidence_android15_cross_origin_readback.png |
Stock Chrome screenshot showing the full fresh UUID in both attacker fields and the attacker JavaScript readback while victim fields are empty. | ea2820fa970bc018884505a5f2fff740073b73f0698f2431d20772bafd15d390 |
evidence_android15_negative_control.png |
Stock Chrome screenshot showing victim-only fill with the attacker frame still reporting ATTACKER_EMPTY. |
2b376b6b85daa0f3772b3c9292d62381c7517e63e4f0a18bff9b018e1942076e |
fix_require_structural_form_similarity.patch |
Minimal proposed browser-process fix that rejects cached-session reuse when whole-form similarity fails. | c46d037e26a9ea20fa697c14b14b7110a564ca62ad057212ea3f53b9ab8358aa |
Impact analysis
Impact
- A cross-origin iframe can receive a victim login form’s authenticated Android Autofill dataset after shifting attacker-controlled fields into cached numeric positions.
- Attacker JavaScript can read the fresh credential values directly from its own fields; the demonstrated UUID was never present before authentication.
- The platform authentication step does not prevent disclosure because the identity loss occurs when Chrome resolves returned cached IDs against the changed current field vector.
- Scope is limited to Android 14/15, user-enabled Android platform Autofill, the stated embedding/delegation layout, normal prefill prediction, and user selection of the presented dataset.
The cause
What version of Chrome have you found the security issue in?
152.0.7977.75 + stable
Is the security issue related to a crash?
No, it is not related to a crash.
Choose the type of vulnerability
Sensitive data exposure
How would you like to be publicly acknowledged for your report?
Anonymous
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://chromium.googlesource.com/chromium/src/+/b7240deab66b4bb9170fdcfe72ccbbed8509f983
- https://chromium.googlesource.com/chromium/src/+log/152cc8489821e33424b31439e17f95c0ab57693a..b7240deab66b4bb9170fdcfe72ccbbed8509f983