Medium watch CVSS 4.3 chrome Policy Bypass 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Autofill
DescriptionInsufficient policy enforcement in Autofill
ComponentAutofill
Bug ClassPolicy Bypass
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker498396238
Fix commitfc0dd2220ced (chromium/src) +17/-10
CWECWE-346 (Origin validation error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-01 (per vendor advisory)
Fix landed2026-04-02 (public git)
Disclosed2026-05-05
Patch gap33 days — fix public in source before the advisory

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.

Key insight
The async “add complement candidate” path captured the import origin at prompt time but never re-checked it against the merger’s current origin before 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

  1. 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.
  2. Navigate to origin B Before accepting (or before the async callback runs), the user is steered to a different origin B.
  3. Submit an incomplete form on B An (incomplete) address submission on B sets the merger’s tracked origin to B.
  4. Stale-origin push The deferred AddMultiStepComplementCandidate fires 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

The patch concretely demonstrates a same-origin-invariant violation in Autofill’s multi-step import queue (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

FunctionChangeNotes
if
components/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 late
    Audit 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 (like Push()’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 queues
    Review other multi-step Autofill/data-merge flows for consistent origin tracking across navigation, especially entry points that bypass the RemoveOutdatedItems-style normalization.
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));
Loading diff…

Original Bug Report

reported by vm...@google.com

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.h
  • components/autofill/core/browser/form_import/form_data_importer_utils.cc
  • components/autofill/core/browser/form_import/addresses/address_profile_save_manager.cc
  • components/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

  1. DCHECK Bypass in TimestampedSameOriginQueue::Push: In components/autofill/core/browser/form_import/form_data_importer_utils.h, the Push() method (lines 51-58) uses a DCHECK to ensure that new items match the existing queue’s origin. In release builds, this DCHECK is a no-op. If a push occurs with a different origin, the cross-origin item is added to the front of the queue, and origin_ is overwritten, but items from the previous origin remain in the queue. A subsequent call to RemoveOutdatedItems() will see the now-matching origin_ and fail to clear the stale cross-origin data.

  2. Unguarded Push via Asynchronous Callback: While the main path (ProcessMultiStepImport) calls RemoveOutdatedItems() before pushing, AddMultiStepImportCandidate() (in form_data_importer_utils.cc) calls Push() directly without any guard. This method is reachable via AddMultiStepComplementCandidate, which is called asynchronously when a user accepts an address save prompt in AddressProfileSaveManager::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.

  3. Cross-Origin Shared State: FormDataImporter is a per-WebContents singleton. All frames within a tab, including cross-origin iframes, share the same MultiStepImportMerger. 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.

  1. Preparation: An attacker on evil.com triggers an address save prompt (e.g., by submitting a form with name/address but empty email/phone). A prompt appears, and the ProfileImportProcess (with origin = evil.com) is stored in an asynchronous callback.
  2. Iframe Submission: The attacker lures the user into submitting a partial form (e.g., email and phone) in a cross-origin victim.com iframe. This submission correctly identifies the origin as victim.com and queues a partial profile. Since the partial profile doesn’t meet minimum requirements, no second prompt is shown to the user.
  3. Queue Corruption: The user clicks “Save” on the original evil.com bubble. The callback executes, calling Push() with the stale evil.com origin. Due to the DCHECK bypass, the evil.com profile is added to the queue containing the victim.com data, and the queue origin is set to evil.com.
  4. Exfiltration: When the attacker submits another form on evil.com, the logic merges the previously queued victim.com data into the attacker’s profile because they are now both associated with the evil.com origin 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.

View on issue tracker