CVE-2026-7986
Overview
Background
- Multi-step address import
- Autofill’s flow that assembles one address profile from data submitted across several forms/steps, tracked by
MultiStepImportMerger. - `TimestampedSameOriginQueue`
- the merger’s queue of candidate items that enforces a same-origin invariant;
Push()DCHECK()s that a newly added item’s origin matches the queue’s currently tracked origin. - `AddMultiStepComplementCandidate`
- a helper (in
address_profile_save_manager.cc) called asynchronously after the user accepts a save prompt, adding the just-imported profile as a complement candidate using the origin of the form it was imported from. - Insufficient policy enforcement
- the bug class here: an origin check that should gate cross-origin mixing of autofill candidate data was missing.
Root Cause Analysis
AddMultiStepComplementCandidate ran asynchronously after the user accepted a save prompt and unconditionally forwarded the imported profile (carrying the import form’s origin) into the merger’s queue via AddMultiStepImportCandidate -> Push(). Because Push() DCHECK()s that the pushed item’s origin equals the queue’s current origin, this was only safe from ProcessMultiStepImport (which first calls RemoveOutdatedItems); from the async save-prompt path it was not guaranteed — if the user navigated to a different site and submitted a different, incomplete address there before the callback fired, the queue’s tracked origin had already changed and the stale-origin profile was pushed into it, firing the DCHECK() (and, in release, mixing data from the old origin into the new origin’s multi-step merge).
The fix renames the helper to MaybeAddMultiStepComplementCandidate and, before pushing, fetches the live multi_step_import_merger() and returns early when import_merger.origin().has_value() && import_merger.origin() != origin, so a profile is only added when it still matches the queue’s current origin. This works by re-validating the same-origin policy at the moment of the async push rather than trusting the origin captured when the prompt was shown.
Push(), so a navigation+submit in between violated the queue’s same-origin invariant; the fix adds an explicit import_merger.origin() != origin early-return guard.Attack Path
- Import a profile on origin A The user fills/submits an address form on an attacker-chosen or first-party origin A, triggering a save prompt whose callback captures origin A.
- Navigate to origin B Before accepting (or before the async callback runs), the user is steered to a different origin B.
- Submit an incomplete form on B An (incomplete) address submission on B sets the merger’s tracked origin to B.
- Stale-origin push
The deferred
AddMultiStepComplementCandidatefires with origin A and pushes the origin-A profile into the queue now tracking origin B, violating the same-origin invariant (DCHECK()failure; cross-origin candidate mixing in release) — the specific downstream autofill consequence is (inferred).
Impact Assessment
DCHECK() failure) reachable by navigating away during the async save-prompt window; in release builds this is insufficient policy enforcement where a profile from one origin could enter another origin’s multi-step merge state, all within the renderer/browser Autofill component. The precondition is user interaction (import prompt followed by navigation and a second incomplete submission). The vendor advisory rates this medium severity (“Insufficient policy enforcement in Autofill”) and does not report in-the-wild exploitation; any further data-leak impact is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/autofill/core/browser/form_import/addresses/address_profile_save_manager.cc |
modified |
Files Changed
components/autofill/core/browser/form_import/addresses/address_profile_save_manager.cc
Audit Directions
- Origin captured early, used lateAudit async callbacks that carry an origin/security principal captured at request time and act on it after possible navigation; re-validate against the current state at use time.
- Invariants enforced only by `DCHECK`Find
DCHECK()-guarded preconditions (likePush()’s same-origin check) whose callers do not all guarantee the condition; a missing caller-side guard is a release-build policy gap. - Multi-step/stateful import queuesReview other multi-step Autofill/data-merge flows for consistent origin tracking across navigation, especially entry points that bypass the
RemoveOutdatedItems-style normalization.
Patch
From fc0dd2220ced570378f79d20c808e691c611e981 Mon Sep 17 00:00:00 2001
From: Florian Leimgruber <fleimgruber@google.com>
Date: Thu, 02 Apr 2026 08:01:50 -0700
Subject: [PATCH] Fix DCHECK() violation in MultiStepImportMerger
See crbug.com/498396238 for a detailed description of the issue.
Essentially:
- TimestampedSameOriginQueue::Push() DCHECKs() that the origin of newly
added items matches the current origin of the queue.
- Push() is called from two places:
- ProcessMultiStepImport(): The DCHECK() is guaranteed, because the
function calls RemoveOutdatedItems().
- AddMultiStepComplementCandidate(): Not guaranteed. This function is
called asynchronously after the user accepts a save prompt, with the
origin of the form that the profile was imported from. If the user
has since navigated away and submitted a different, incomplete form
on the new site, the DCHECK() will fail.
To fix it, this CL only calls Push() when the origin still matches. This
means that if the user has since navigated away, the imported profile is
not pushed into the queue and won't support multi-step complement. This
seems more reasonable than clearing the candidates that exist on the new
origin.
Fixed: 498396238
Change-Id: If180c7d4ca2dc2833156ce156be9515dfb41bacc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7720882
Commit-Queue: Florian Leimgruber <fleimgruber@google.com>
Reviewed-by: Jihad Hanna <jihadghanna@google.com>
Cr-Commit-Position: refs/heads/main@{#1609213}
---
diff --git a/components/autofill/core/browser/form_import/addresses/address_profile_save_manager.cc b/components/autofill/core/browser/form_import/addresses/address_profile_save_manager.cc
index 2387c70..533b4646 100644
--- a/components/autofill/core/browser/form_import/addresses/address_profile_save_manager.cc
+++ b/components/autofill/core/browser/form_import/addresses/address_profile_save_manager.cc
@@ -23,12 +23,21 @@
// with additional optional information.
// This function adds the imported profile as a candidate. This is only done
// after the user decision to incorporate manual edits.
-void AddMultiStepComplementCandidate(FormDataImporter* form_data_importer,
- const AutofillProfile& profile,
- const url::Origin& origin) {
+void MaybeAddMultiStepComplementCandidate(FormDataImporter* form_data_importer,
+ const AutofillProfile& profile,
+ const url::Origin& origin) {
if (!form_data_importer) {
return;
}
+ MultiStepImportMerger& import_merger =
+ form_data_importer->GetAddressFormDataImporter()
+ .multi_step_import_merger();
+ // Avoid adding profiles that don't match the currently tracked origin. It is
+ // possible that the user has navigated away since the import prompt was shown
+ // and submitted an (incomplete) address on the new origin in the meantime.
+ if (import_merger.origin().has_value() && import_merger.origin() != origin) {
+ return;
+ }
// Metrics depending on `import_process.import_metadata()` are collected
// for the `confirmed_import_candidate`. E.g. whether the removal of an
// invalid phone number made the import possible. Just like regular updates,
@@ -36,10 +45,8 @@
// The `import_metadata` is thus initialized to a neutral element.
ProfileImportMetadata import_metadata;
import_metadata.origin = origin;
- form_data_importer->GetAddressFormDataImporter()
- .multi_step_import_merger()
- .AddMultiStepImportCandidate(profile, import_metadata,
- /*is_imported=*/true);
+ import_merger.AddMultiStepImportCandidate(profile, import_metadata,
+ /*is_imported=*/true);
}
AutofillClient::SaveAddressBubbleType AutofillProfileImportTypeToBubbleType(
@@ -147,9 +154,9 @@
const std::optional<AutofillProfile>& confirmed_import_candidate =
import_process->confirmed_import_candidate();
DCHECK(confirmed_import_candidate);
- AddMultiStepComplementCandidate(client_->GetFormDataImporter(),
- *confirmed_import_candidate,
- import_process->import_metadata().origin);
+ MaybeAddMultiStepComplementCandidate(
+ client_->GetFormDataImporter(), *confirmed_import_candidate,
+ import_process->import_metadata().origin);
}
ClearPendingImport(std::move(import_process));
Original Bug Report
Cross-origin Autofill data leak via TimestampedSameOriginQueue origin check 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 security team.
Overview: A potential cross-origin profile data leak exists in the Autofill MultiStepImportMerger due to an insufficient origin check in TimestampedSameOriginQueue::Push. By exploiting an asynchronous save prompt callback, an attacker could corrupt the multi-step import queue, leading to the merge of victim data into an attacker-controlled profile.
Affected files:
components/autofill/core/browser/form_import/form_data_importer_utils.hcomponents/autofill/core/browser/form_import/form_data_importer_utils.cccomponents/autofill/core/browser/form_import/addresses/address_profile_save_manager.cccomponents/autofill/core/browser/form_import/addresses/address_form_data_importer.cc
Estimated timestamp from git blame: 2024-11-20
Summary
A potential vulnerability in Chrome’s Autofill system could allow an attacker-controlled site to exfiltrate sensitive user data (such as email and phone numbers) typed into a cross-origin iframe. The issue is caused by an insufficient origin check in TimestampedSameOriginQueue::Push, which enforces its same-origin invariant using only a DCHECK. In release builds, this check is bypassed, allowing the internal state of the multi-step import queue to be corrupted by asynchronous callbacks with stale origin information.
Technical Details
-
DCHECK Bypass in
TimestampedSameOriginQueue::Push: Incomponents/autofill/core/browser/form_import/form_data_importer_utils.h, thePush()method (lines 51-58) uses aDCHECKto ensure that new items match the existing queue’s origin. In release builds, thisDCHECKis a no-op. If a push occurs with a different origin, the cross-origin item is added to the front of the queue, andorigin_is overwritten, but items from the previous origin remain in the queue. A subsequent call toRemoveOutdatedItems()will see the now-matchingorigin_and fail to clear the stale cross-origin data. -
Unguarded Push via Asynchronous Callback: While the main path (
ProcessMultiStepImport) callsRemoveOutdatedItems()before pushing,AddMultiStepImportCandidate()(inform_data_importer_utils.cc) callsPush()directly without any guard. This method is reachable viaAddMultiStepComplementCandidate, which is called asynchronously when a user accepts an address save prompt inAddressProfileSaveManager::FinalizeProfileImport(address_profile_save_manager.cc). The origin used in this callback is captured when the prompt is first shown and is not updated if the user navigates or interacts with cross-origin content in the meantime. -
Cross-Origin Shared State:
FormDataImporteris a per-WebContentssingleton. All frames within a tab, including cross-origin iframes, share the sameMultiStepImportMerger. This allows a victim’s form submission in an iframe to populate the same queue that is currently awaiting a user decision on an address prompt from the main frame (e.g.,evil.com).
Potential Attack Scenario
Note: These are suggested steps; our tooling has not executed this as a live proof-of-concept.
- Preparation: An attacker on
evil.comtriggers an address save prompt (e.g., by submitting a form with name/address but empty email/phone). A prompt appears, and theProfileImportProcess(withorigin = evil.com) is stored in an asynchronous callback. - Iframe Submission: The attacker lures the user into submitting a partial form (e.g., email and phone) in a cross-origin
victim.comiframe. This submission correctly identifies the origin asvictim.comand queues a partial profile. Since the partial profile doesn’t meet minimum requirements, no second prompt is shown to the user. - Queue Corruption: The user clicks “Save” on the original
evil.combubble. The callback executes, callingPush()with the staleevil.comorigin. Due to theDCHECKbypass, theevil.comprofile is added to the queue containing thevictim.comdata, and the queue origin is set toevil.com. - Exfiltration: When the attacker submits another form on
evil.com, the logic merges the previously queuedvictim.comdata into the attacker’s profile because they are now both associated with theevil.comorigin state. The merged profile, containing the victim’s email/phone, is saved to the user’s profile storage and can be exfiltrated via JS when the user next uses Autofill on the attacker’s site.
Suggested Fix
Replace the DCHECK(!origin_ || *origin_ == item_origin); in TimestampedSameOriginQueue::Push with a runtime CHECK or explicitly call RemoveOutdatedItems() inside Push() if the origin_ does not match the incoming item_origin. Alternatively, ensure that AddMultiStepComplementCandidate validates the current queue origin against its stored origin before pushing.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.