CVE-2026-13826
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 0115ebdbf25d6c907cdbbfa7b7d9206b502c56d8 Mon Sep 17 00:00:00 2001 From: Jihad Hanna <[email protected]> Date: Wed, 03 Jun 2026 02:15:47 -0700 Subject: [PATCH] Fix state mismatch between Autofill Session and Credential Manager The three bug reports linked below were about stale/incorrect state management for AndroidAutofillProvider::session_state_. This CL adds state handling to avoid the exploitations described in the reports. It also adds regression tests for the three described scenarios. Fixed: 513237800, 518084475, 518115316 Change-Id: I9a1143804764a9473e2fc4f3e84ea44387d76bbc Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7889818 Reviewed-by: Dominic Battré <[email protected]> Commit-Queue: Jihad Hanna <[email protected]> Cr-Commit-Position: refs/heads/main@{#1640793} --- diff --git a/components/android_autofill/browser/android_autofill_provider.cc b/components/android_autofill/browser/android_autofill_provider.cc index 58af3927..ad881906 100644 --- a/components/android_autofill/browser/android_autofill_provider.cc +++ b/components/android_autofill/browser/android_autofill_provider.cc @@ -200,7 +200,13 @@ DCHECK_CURRENTLY_ON(BrowserThread::UI); if (credman_sheet_status_ == CredManBottomSheetLifecycle::kIsShowing) { - return; // CredMan prevents 3P autofill UI. Start the session on refocus! + // While CredMan is active, the user cannot legitimately interact with the + // page. We ignore this request to prevent a compromised renderer from + // spoofing the session origin (overwriting `current_field`) in the + // background. We preserve the session state that triggered CredMan so that + // the subsequent fill goes to the correct frame. If the user dismisses + // CredMan, a new session will be started on the next focus event. + return; } // We need to create session state here outside of StartNewSession because @@ -498,6 +504,13 @@ AndroidAutofillManager* manager, const FormData& form, const FormFieldData& field) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (credman_sheet_status_ == CredManBottomSheetLifecycle::kIsShowing) { + // Ignore selection changes while CredMan is showing to prevent a + // compromised renderer from spoofing the session origin (see + // crbug.com/518115316). + return; + } if (base::FeatureList::IsEnabled( features::kAndroidAutofillFieldsUpdatedOnSelect)) { UpdateCurrentField(manager, form, field); @@ -566,6 +579,11 @@ GetRenderFrameHost(manager, field.host_frame()); ShouldShowCredManForField(field, rfh) && ShowCredManSheet(rfh, form.global_id(), field_to_focus)) { + // Proactively update the current field and its origin. Because the + // subsequent `OnAskForValuesToFill()` IPC will be ignored while CredMan is + // showing (to block spoofing), we must set the correct origin now before + // the block takes effect, otherwise the session will retain a stale origin. + UpdateCurrentField(manager, form, field); return; // The focus event will be completed after CredMan closes. } if (field_to_focus) { diff --git a/components/android_autofill/browser/android_autofill_provider_unittest.cc b/components/android_autofill/browser/android_autofill_provider_unittest.cc index 79a3d526..c692e59 100644 --- a/components/android_autofill/browser/android_autofill_provider_unittest.cc +++ b/components/android_autofill/browser/android_autofill_provider_unittest.cc @@ -837,12 +837,15 @@ autofill_provider().MaybeInitKeyboardSuppressor(); // Navigation creates the AndroidAutofillManager for the main frame. - NavigateAndCommit(GURL("about:blank")); + NavigateAndCommit(GURL("https://foo.com")); FocusWebContentsOnMainFrame(); // Load a form with webuthn-annotated username and regular password fields. test_webauthn_form_ = CreateFormDataForFrame( CreateTestWebAuthnPasswordFormData(), main_frame_token()); + url::Origin foo_origin = url::Origin::Create(GURL("https://foo.com")); + test_api(test_webauthn_form_).field(0).set_origin(foo_origin); + test_api(test_webauthn_form_).field(1).set_origin(foo_origin); InitializeWebAuthnFactoryWithMock(); } @@ -1074,6 +1077,9 @@ sub_frame_webauthn_form_ = CreateFormDataForFrame( CreateTestWebAuthnPasswordFormData(), LocalFrameToken(sub_frame_->GetFrameToken().value())); + url::Origin bar_origin = url::Origin::Create(GURL("https://bar.com")); + test_api(sub_frame_webauthn_form_).field(0).set_origin(bar_origin); + test_api(sub_frame_webauthn_form_).field(1).set_origin(bar_origin); android_autofill_manager().OnFormsSeen({sub_frame_webauthn_form_}, /*removed_forms=*/{}); @@ -1133,6 +1139,97 @@ FocusSubFrameFormField(sub_frame_webauthn_email_field()); } +// Tests that when CredMan is triggered, the current field's origin is updated +// proactively to the frame origin of the passkey field, ensuring we don't use +// a stale origin if CredMan is dismissed and autofill completes. +// (see crbug.com/518084475). +TEST_F(AndroidAutofillProviderWithCredManMultiFrameTest, + CredManEarlyReturnLeavesStaleCurrentFieldOrigin_Fixed) { + // 1. Start session on main frame (origin https://foo.com). + android_autofill_manager().OnFormsSeen({test_form()}, {}); + // Focus main frame field to start session and set origin to foo.com. + android_autofill_manager().SimulateOnAskForValuesToFill( + test_form(), non_webauthn_password_field()); + android_autofill_manager().SimulateOnFocusOnFormField( + test_form(), non_webauthn_password_field()); + ASSERT_EQ(test_api(autofill_provider()).last_focused_field_origin(), + url::Origin::Create(GURL("https://foo.com"))); + + // 2. Focus subframe field (origin https://bar.com) which triggers CredMan. + // Expect CredMan to be triggered on subframe. + EXPECT_CALL(*sub_frame_mock_delegate_, TriggerCredManUi); + + // Simulate Focus FIRST (which should update origin to bar.com). + android_autofill_manager().SimulateOnFocusOnFormField( + sub_frame_test_form(), sub_frame_webauthn_email_field()); + EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(), + url::Origin::Create(GURL("https://bar.com"))); + + // Simulate AskForValuesToFill() SECOND (which returns early because CredMan + // is showing) and verify origin is STILL bar.com (not reverted or stale + // foo.com). + android_autofill_manager().SimulateOnAskForValuesToFill( + sub_frame_test_form(), sub_frame_webauthn_email_field()); + EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(), + url::Origin::Create(GURL("https://bar.com"))); +} + +// Tests that a compromised renderer cannot spoof the session origin by sending +// a malicious AskForValuesToFill IPC while a CredMan sheet is active. +// (see crbug.com/513237800). +TEST_F(AndroidAutofillProviderWithCredManMultiFrameTest, + CredManActiveBlocksSpoofedAskForValuesToFill) { + // 1. Start session on main frame (origin https://foo.com) and trigger + // CredMan. + android_autofill_manager().OnFormsSeen({test_form()}, {}); + EXPECT_CALL(cred_man_delegate(), TriggerCredManUi); + android_autofill_manager().SimulateOnFocusOnFormField(test_form(), + webauthn_email_field()); + android_autofill_manager().SimulateOnAskForValuesToFill( + test_form(), webauthn_email_field()); + ASSERT_EQ(test_api(autofill_provider()).last_focused_field_origin(), + url::Origin::Create(GURL("https://foo.com"))); + + // 2. Spoof AskForValuesToFill() from attacker.com: Attacker sends fake + // AskForValuesToFill() while CredMan is showing. Verify that origin remains + // foo.com and that the spoof is blocked. + android_autofill_manager().SimulateOnAskForValuesToFill( + sub_frame_test_form(), sub_frame_webauthn_email_field()); + EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(), + url::Origin::Create(GURL("https://foo.com"))); +} + +// Tests that a compromised renderer cannot spoof the session origin by sending +// a malicious SelectControlSelectionChanged() IPC while a CredMan sheet is +// active (see crbug.com/518115316). +TEST_F(AndroidAutofillProviderWithCredManMultiFrameTest, + CredManActiveBlocksSpoofedSelectControlSelectionChanged) { + base::test::ScopedFeatureList scoped_feature_list{ + features::kAndroidAutofillFieldsUpdatedOnSelect}; + + // 1. Start session on main frame (origin https://foo.com) and trigger + // CredMan. + android_autofill_manager().OnFormsSeen({test_form()}, {}); + EXPECT_CALL(cred_man_delegate(), TriggerCredManUi); + android_autofill_manager().SimulateOnFocusOnFormField(test_form(), + webauthn_email_field()); + android_autofill_manager().SimulateOnAskForValuesToFill( + test_form(), webauthn_email_field()); + ASSERT_EQ(test_api(autofill_provider()).last_focused_field_origin(), + url::Origin::Create(GURL("https://foo.com"))); + + // 2. Spoof SelectControlSelectionChanged() from attacker.com: Attacker sends + // fake SelectControlSelectionChanged() while CredMan is showing. Verify that + // origin remains foo.com and that the spoof is blocked. + autofill_provider().OnSelectControlSelectionChanged( + &android_autofill_manager(), sub_frame_test_form(), + sub_frame_webauthn_email_field()); + + // Verify origin remains foo.com (spoof blocked!). + EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(), + url::Origin::Create(GURL("https://foo.com"))); +} + using AndroidAutofillProviderPrefillRequestTest = AndroidAutofillProviderTest; // Tests that we can send another prefill request after navigation.
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 79a3d526..c692e59 100644
--- a/components/android_autofill/browser/android_autofill_provider_unittest.cc
+++ b/components/android_autofill/browser/android_autofill_provider_unittest.cc
@@ -837,12 +837,15 @@
autofill_provider().MaybeInitKeyboardSuppressor();
// Navigation creates the AndroidAutofillManager for the main frame.
- NavigateAndCommit(GURL("about:blank"));
+ NavigateAndCommit(GURL("https://foo.com"));
FocusWebContentsOnMainFrame();
// Load a form with webuthn-annotated username and regular password fields.
test_webauthn_form_ = CreateFormDataForFrame(
CreateTestWebAuthnPasswordFormData(), main_frame_token());
+ url::Origin foo_origin = url::Origin::Create(GURL("https://foo.com"));
+ test_api(test_webauthn_form_).field(0).set_origin(foo_origin);
+ test_api(test_webauthn_form_).field(1).set_origin(foo_origin);
InitializeWebAuthnFactoryWithMock();
}
@@ -1074,6 +1077,9 @@
sub_frame_webauthn_form_ = CreateFormDataForFrame(
CreateTestWebAuthnPasswordFormData(),
LocalFrameToken(sub_frame_->GetFrameToken().value()));
+ url::Origin bar_origin = url::Origin::Create(GURL("https://bar.com"));
+ test_api(sub_frame_webauthn_form_).field(0).set_origin(bar_origin);
+ test_api(sub_frame_webauthn_form_).field(1).set_origin(bar_origin);
android_autofill_manager().OnFormsSeen({sub_frame_webauthn_form_},
/*removed_forms=*/{});
@@ -1133,6 +1139,97 @@
FocusSubFrameFormField(sub_frame_webauthn_email_field());
}
+// Tests that when CredMan is triggered, the current field's origin is updated
+// proactively to the frame origin of the passkey field, ensuring we don't use
+// a stale origin if CredMan is dismissed and autofill completes.
+// (see crbug.com/518084475).
+TEST_F(AndroidAutofillProviderWithCredManMultiFrameTest,
+ CredManEarlyReturnLeavesStaleCurrentFieldOrigin_Fixed) {
+ // 1. Start session on main frame (origin https://foo.com).
+ android_autofill_manager().OnFormsSeen({test_form()}, {});
+ // Focus main frame field to start session and set origin to foo.com.
+ android_autofill_manager().SimulateOnAskForValuesToFill(
+ test_form(), non_webauthn_password_field());
+ android_autofill_manager().SimulateOnFocusOnFormField(
+ test_form(), non_webauthn_password_field());
+ ASSERT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ url::Origin::Create(GURL("https://foo.com")));
+
+ // 2. Focus subframe field (origin https://bar.com) which triggers CredMan.
+ // Expect CredMan to be triggered on subframe.
+ EXPECT_CALL(*sub_frame_mock_delegate_, TriggerCredManUi);
+
+ // Simulate Focus FIRST (which should update origin to bar.com).
+ android_autofill_manager().SimulateOnFocusOnFormField(
+ sub_frame_test_form(), sub_frame_webauthn_email_field());
+ EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ url::Origin::Create(GURL("https://bar.com")));
+
+ // Simulate AskForValuesToFill() SECOND (which returns early because CredMan
+ // is showing) and verify origin is STILL bar.com (not reverted or stale
+ // foo.com).
+ android_autofill_manager().SimulateOnAskForValuesToFill(
+ sub_frame_test_form(), sub_frame_webauthn_email_field());
+ EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ url::Origin::Create(GURL("https://bar.com")));
+}
+
+// Tests that a compromised renderer cannot spoof the session origin by sending
+// a malicious AskForValuesToFill IPC while a CredMan sheet is active.
+// (see crbug.com/513237800).
+TEST_F(AndroidAutofillProviderWithCredManMultiFrameTest,
+ CredManActiveBlocksSpoofedAskForValuesToFill) {
+ // 1. Start session on main frame (origin https://foo.com) and trigger
+ // CredMan.
+ android_autofill_manager().OnFormsSeen({test_form()}, {});
+ EXPECT_CALL(cred_man_delegate(), TriggerCredManUi);
+ android_autofill_manager().SimulateOnFocusOnFormField(test_form(),
+ webauthn_email_field());
+ android_autofill_manager().SimulateOnAskForValuesToFill(
+ test_form(), webauthn_email_field());
+ ASSERT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ url::Origin::Create(GURL("https://foo.com")));
+
+ // 2. Spoof AskForValuesToFill() from attacker.com: Attacker sends fake
+ // AskForValuesToFill() while CredMan is showing. Verify that origin remains
+ // foo.com and that the spoof is blocked.
+ android_autofill_manager().SimulateOnAskForValuesToFill(
+ sub_frame_test_form(), sub_frame_webauthn_email_field());
+ EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ url::Origin::Create(GURL("https://foo.com")));
+}
+
+// Tests that a compromised renderer cannot spoof the session origin by sending
+// a malicious SelectControlSelectionChanged() IPC while a CredMan sheet is
+// active (see crbug.com/518115316).
+TEST_F(AndroidAutofillProviderWithCredManMultiFrameTest,
+ CredManActiveBlocksSpoofedSelectControlSelectionChanged) {
+ base::test::ScopedFeatureList scoped_feature_list{
+ features::kAndroidAutofillFieldsUpdatedOnSelect};
+
+ // 1. Start session on main frame (origin https://foo.com) and trigger
+ // CredMan.
+ android_autofill_manager().OnFormsSeen({test_form()}, {});
+ EXPECT_CALL(cred_man_delegate(), TriggerCredManUi);
+ android_autofill_manager().SimulateOnFocusOnFormField(test_form(),
+ webauthn_email_field());
+ android_autofill_manager().SimulateOnAskForValuesToFill(
+ test_form(), webauthn_email_field());
+ ASSERT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ url::Origin::Create(GURL("https://foo.com")));
+
+ // 2. Spoof SelectControlSelectionChanged() from attacker.com: Attacker sends
+ // fake SelectControlSelectionChanged() while CredMan is showing. Verify that
+ // origin remains foo.com and that the spoof is blocked.
+ autofill_provider().OnSelectControlSelectionChanged(
+ &android_autofill_manager(), sub_frame_test_form(),
+ sub_frame_webauthn_email_field());
+
+ // Verify origin remains foo.com (spoof blocked!).
+ EXPECT_EQ(test_api(autofill_provider()).last_focused_field_origin(),
+ url::Origin::Create(GURL("https://foo.com")));
+}
+
using AndroidAutofillProviderPrefillRequestTest = AndroidAutofillProviderTest;
// Tests that we can send another prefill request after navigation.
Original Bug Report
Potential logic error in AndroidAutofillProvider desyncs session state allowing cross-origin bypass
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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential logic error in AndroidAutofillProvider::OnAskForValuesToFill allows a compromised renderer to spoof the triggered origin of an active Autofill session. This occurs because the internal session origin is updated before checking the status of the Credential Manager UI, leading to state desynchronization. This could potentially allow an attacker to bypass cross-origin Autofill security policies and leak sensitive user data.
Affected files:
components/android_autofill/browser/android_autofill_provider.cccomponents/android_autofill/browser/android_autofill_manager.cccomponents/autofill/content/browser/content_autofill_driver.cccomponents/autofill/core/browser/foundations/form_forest.cc
Estimated timestamp from git blame: 2024-09-18
Root Cause Analysis
The potential vulnerability is located in AndroidAutofillProvider::OnAskForValuesToFill within components/android_autofill/browser/android_autofill_provider.cc. The function updates the session_state_->current_field.origin before performing an early-return check for the Credential Manager (CredMan) UI status.
// components/android_autofill/browser/android_autofill_provider.cc
void AndroidAutofillProvider::OnAskForValuesToFill(...) {
// ...
if (!session_state_) {
session_state_.emplace();
}
// ...
UpdateCurrentField(manager, form, field); // Updates origin unconditionally
if (credman_sheet_status_ == CredManBottomSheetLifecycle::kIsShowing) {
return; // Early return if CredMan UI is showing
}
if (!IsLinkedForm(form)) {
StartNewSession(manager, form, field); // Resets manager and form data
}
}
When UpdateCurrentField is called, session_state_->current_field.origin is overwritten with the origin provided in the renderer’s IPC. If credman_sheet_status_ is kIsShowing, the function returns before StartNewSession (or Reset) can be called. This results in a state where session_state_->current_field.origin reflects a new origin, while session_state_->manager and session_state_->form still point to the previous, legitimate session.
Potential Exploit Scenario
An attacker with control over a renderer (e.g., via a compromised subframe or a malicious site) could potentially follow these steps:
- A user interacts with a legitimate form (e.g., a login form in a bank’s iframe) that triggers a CredMan passkey selection UI.
AndroidAutofillProviderinitializes a session for the bank’s origin. - While the CredMan UI is visible, a compromised renderer sends a malicious
AskForValuesToFillIPC for a field under the attacker’s control. AndroidAutofillProvider::OnAskForValuesToFillupdates the session’s origin to the attacker’s origin but returns early due to the active CredMan UI, leaving the bank’s manager and form active in the session state.- The user dismisses CredMan and selects a standard password autofill suggestion for the bank’s form from the keyboard accessory.
- The browser executes the fill operation using the desynchronized state, passing the attacker’s origin as the
triggered_origintoFormForest::IsSafeToFill. - Security checks in
FormForest::IsSafeToFill(Clause 2 or 4) are bypassed because the spoofedtriggered_originmatches the attacker’s fields in the form structure, causing sensitive data to be leaked into the attacker’s fields.
Suggested Fix
To prevent this desynchronization, the update to the session’s field state should be performed after the CredMan status check, or the entire state should be properly reset/updated before the early return. Specifically, consider moving the call to UpdateCurrentField after the check for credman_sheet_status_ == kIsShowing.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.