Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Passwords
DescriptionUse after free in Passwords
ComponentPasswords
Sub-componentUI>Browser>Passwords
Bug ClassUAF
Tracker558377544
Fix commite6113c591043 (chromium/src) +92/-39
CISA KEVNot listed
CreditedOGINOME Tomohito
Disclosed2026-10-20

Files Changed

  • components/password_manager/core/browser/password_autofill_manager_unittest.cc
  • components/password_manager/core/browser/password_autofill_manager.cc
  • components/password_manager/core/browser/password_autofill_manager.h
  • components/password_manager/core/browser/BUILD.gn

Original Bug Report

reported by [email protected]

Access to cross-site data: on Android Stable a compromised renderer receives in plaintext a password saved for another origin; the cross-domain consent guard is compiled out of that fill path.


Report description

Access to cross-site data: on Android Stable a compromised renderer receives in plaintext a password saved for another origin; the cross-domain consent guard is compiled out of that fill path.


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://chromium.googlesource.com/chromium/src/+/HEAD/components/password_manager/core/browser/password_manual_fallback_flow.cc


The problem

Please describe the technical details of the vulnerability

TL;DR

Shipping on Android Stable today, with no flag, policy or origin trial: 152.0.7977.x (M152) and 153.0.8010.27 (M153). A compromised renderer sends one message; the browser fills a password saved for a different site into the attacker’s form, in plaintext, on one tap:

AskForValuesToFill(form, field_id, caret_bounds, trigger_source,          // attacker's renderer
    password_request{ field.trigger_source = kManualFallbackPasswords,    // the one changed field
                      field.element_id     = the attacker's password input })
// browser: components/password_manager/core/browser/password_manual_fallback_flow.cc
 503  #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS)
 505    if (payload.is_cross_domain) { ShowCrossDomainConfirmationPopup(...); return; }
 515  #endif
 518    std::move(on_allowed).Run();   // Android lands here: the plaintext goes to the renderer

Confidentiality: the attacker’s origin obtains, in plaintext, passwords the victim saved for other origins, one per tap. Integrity: none.

Attack scenario

Attacker. Whoever serves attacker.example and has compromised the renderer process that legitimately loaded their own page — not a WebUI process, not a spare renderer, not an extension process, no flag and no policy.

Victim. Anyone on Chrome for Android using Chrome’s built-in password manager with at least one password saved for some other site.

What the attacker does. Sends one AskForValuesToFill carrying password_request.field.trigger_source = kManualFallbackPasswords; the message is otherwise shaped exactly like an ordinary click, and nothing between the wire and the fill re-derives that value.

What the victim does. Taps a password field on the attacker’s page, then taps one chip on the keyboard accessory bar — the ordinary Android gesture for filling a saved password.

Starting position. The compromised renderer can already obtain credentials saved for its own origin through ordinary autofill; what it gains here is credentials saved for other origins.

Victim assumption. The victim is not asked to navigate away, install anything, or accept any prompt — the one prompt Chrome normally shows here, the sheet that names the other site, is precisely what does not appear.

Reproduction

Measured 2026-08-26 on a Pixel 8 Pro (Android 17, build CP2A.260805.005, security patch 2026-08-05), in a dedicated secondary Android user, not signed in, no command-line flags. Every value below is transcribed in android-manual-fallback-cross-domain-fill.txt.

  1. Build. Build Chrome for Android from base commit ce7f1dd1ae9bcb33571f4bd9e0d3a8953c217491 with poc-compromised-renderer.patch applied — one renderer-side sender, confined to a process whose type is renderer, emitting the message in step 5. On a public Chromium build also apply poc-local-store.patch, which supplies the credential store Android otherwise lacks. Both patches are described in analysis.md. Expected: installs as org.chromium.chrome, versionName 154.0.8024.0; no flags.
  2. Victim data. Save one password for victim-site.example, nothing for attacker.example. Expected: exactly one row — measured http://a.127-0-0-1.nip.io:8080/|victim-user, plaintext Tr1cky-Fern-8823.
  3. Page. Load attacker.example, an ordinary page with a username input and an input type="password". Expected: it loads in a normal web renderer, with no prompt of any kind.
  4. Control. Load the identical page at /control, whose password input carries no attribute for the reproduction hook, and tap its password input. Expected: the field is focused and nothing is offered — measured, the view hierarchy at that moment contains the other origin’s name 0 times and the password 0 times.
  5. The message. Back on the page from step 3, the compromised renderer sends AskForValuesToFill with password_request.field.trigger_source = kManualFallbackPasswords and .element_id = the password input’s renderer id; every other field is what an ordinary click would have sent. Expected: the renderer is not killed for a bad message — measured, the browser log matches bad.?message|killing renderer|terminating renderer 0 times and the sandboxed renderers survive.
  6. The list. Tap the password input and read the keyboard accessory bar above the keyboard. Expected: a chip appears for a credential saved for another site — measured text="http://a.127-0-0-1.nip.io:8080" next to text="victim-u". Steps 4 and 6 differ in exactly one thing: the suggestion trigger source.
  7. The tap. Tap that chip. ← BUG FIRES HERE Expected: the “Use your victim-site.example password?” bottom sheet (android_chrome_strings.grd:928-939) does not appear and the password field is filled — measured, the dump taken after the tap holds text="Tr1cky-Fern-8823" class="android.widget.EditText" password="true" and contains none of the sheet’s four strings.
  8. The read-out. From the attacker’s page, read the password input’s value. Expected: Tr1cky-Fern-8823 — the plaintext saved for victim-site.example, read by script running on the attacker’s origin.
  9. Fix arm. Repeat steps 3–8 on the same build with proposed-patch.diff also applied. Expected: the chip is still offered, but the tap now shows the sheet naming both origins and the field stays empty until it is accepted — measured; the two builds differ only by that patch.

Root cause

Both the routing and the value come from the renderer, and nothing between the wire and the sink re-derives either:

  • components/autofill/content/common/mojom/autofill_driver.mojom:113-117 — AskForValuesToFill(FormData form, FieldRendererId field_id, gfx.mojom.Rect caret_bounds, AutofillSuggestionTriggerSource trigger_source, PasswordSuggestionRequest? password_request); — password_request is supplied by the renderer in full.
  • components/autofill/core/common/mojom/autofill_types_mojom_traits.cc:602-609 — return data.ReadField(&out->field) && data.ReadFormData(&out->form_data) && ...; — deserialization reads the fields and checks nothing about them.
  • components/autofill/content/browser/content_autofill_driver.cc:303 — if (!bad_message::CheckArgs(args...) || ...) { return; } — and components/autofill/content/browser/bad_message.cc:18-25 CheckFieldInForm validates only the top-level FieldRendererId, not the one nested in password_request.field.
  • components/autofill/core/browser/foundations/browser_autofill_manager.cc:1083-1087 — #if !BUILDFLAG(IS_ANDROID) password_delegate->ShowSuggestions(password_request->field); #else password_delegate->ShowKeyboardReplacingSurface(password_request.value()); #endif — the branch that is meant to keep Android off the desktop path.
  • chrome/browser/password_manager/chrome_password_manager_client.cc:619-620 and :724 — driver->GetPasswordAutofillManager()->ShowSuggestions(request.field); — two Android-only lines that call the desktop entry point anyway. :724 is taken whenever Touch-To-Fill has no credential for the current origin (touch_to_fill_password_manager_controller.cc:116-123 returns false), which is always true on a domain with nothing saved.
  • components/password_manager/core/browser/password_autofill_manager.cc:578 — if (autofill::IsPasswordsAutofillManuallyTriggered(field.trigger_source)) { — and :590-601 constructs PasswordManualFallbackFlow and runs it, with no platform condition.
  • components/password_manager/core/browser/password_manual_fallback_flow.cc:309-321 — EnsureCrossDomainPasswordUsageGetsConsent(payload, /*on_allowed=*/base::BindOnce(... &PasswordManagerDriver::FillSuggestionById, ..., payload.password, ...));
  • password_manual_fallback_flow.cc:503-504 — #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS) — no IS_ANDROID, so the whole if (payload.is_cross_domain) body is compiled out and control reaches :518 std::move(on_allowed).Run();.
  • components/password_manager/content/browser/content_password_manager_driver.cc:325-327 — GetPasswordAutofillAgent()->FillPasswordSuggestionById(username_element_id, password_element_id, username, password, suggestion_source); — the plaintext is the IPC payload itself.

Of the four preprocessor conditions in this file, :503-515 is the only one wrapping a check; the other three wrap a helper definition (:54-90), a different suggestion type’s browser-side action (:352-362) and a re-auth message string (:467-472), so on an excluded platform those degrade while :503 fails open. The owning team’s own guard for this decision is password_client_->ShowCrossDomainConfirmationPopup(...), and the sibling call sites in the same directory place an #if that does list IS_ANDROID inside if (payload.is_cross_domain) { ... } else { fill }, so an excluded platform fills nothing rather than filling without consent (password_autofill_manager.cc:410-428, :461-482); two further Android paths do the same job with the Android sheet (touch_to_fill_password_manager_controller.cc:180-196, password_accessory_controller_impl.cc:845-856). This fill path is also the widest of the four, not the narrowest: the offered set is the whole store (password_manual_fallback_flow.cc:121, password_suggestion_generator.cc:664-667). Full comparison in analysis.md.

The #if was never a decision that Android does not need consent. When the guard was extended to kPasswordEntry suggestions in https://chromium-review.googlesource.com/c/chromium/src/+/5783088 (merged 2024-08-21), a reviewer asked about this exact line and the answer is on the public record — password_manual_fallback_flow.cc, patch set 2, line 390 (the #if continuation line):

> reviewer: “why is this needed? Can this code even be triggered outside desktop?” > author: “triggered - not, but compiled with it”

The fall-through therefore rests on a reachability claim, not a policy: the class is believed not to be triggered outside desktop. chrome_password_manager_client.cc:619 and :724 are the counterexample — both sit inside that file’s #if BUILDFLAG(IS_ANDROID) block (:546-727) and both call ShowSuggestions, the desktop entry point, on Android. Chrome’s own threat model states the boundary this crosses: docs/security/compromised-renderers.md:136-137 — “Compromised renderers shouldn’t be able to read or write passwords of other sites.”

Line numbers are from origin/main at b8885e8f2ca63ea5439a88bd0fcb2ece7836ce5b. In the guard’s own file every cited line has the same number on branch-heads/7977 (M152) and branch-heads/8010 (M153), the two branches on Android Stable today. Per-branch table in analysis.md.

Suggested fix

Give the guard the condition its own interface already uses. ShowCrossDomainConfirmationPopup is declared for every platform except iOS (password_manager_client.h:600-609), so the single call site that narrows it to four desktop platforms should say !BUILDFLAG(IS_IOS) as well; the sibling already includes the controller header unconditionally (password_autofill_manager.h:31), so the header’s include guard can go with it. This adds no UI: that method’s implementation (chrome/browser/password_manager/chrome_password_manager_client.cc:1506-1526) already has a complete Android branch that shows the acknowledge sheet and runs the confirmation callback only on DismissReason::kAccept — the same Android acknowledge sheet the three sibling paths already show. The complete patch is attached as proposed-patch.diff; the hunk that matters is:

--- a/components/password_manager/core/browser/password_manual_fallback_flow.cc
+++ b/components/password_manager/core/browser/password_manual_fallback_flow.cc
@@ -500,8 +500,7 @@ void PasswordManualFallbackFlow::CancelBiometricReauthIfOngoing() {
 void PasswordManualFallbackFlow::EnsureCrossDomainPasswordUsageGetsConsent(
     const Suggestion::PasswordSuggestionDetails& payload,
     base::OnceClosure on_allowed) {
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || \
-    BUILDFLAG(IS_CHROMEOS)
+#if !BUILDFLAG(IS_IOS)
   if (payload.is_cross_domain) {
     CHECK(payload.display_signon_realm);
     cross_domain_confirmation_popup_controller_ =

Prior reports this is not

https://issues.chromium.org/issues/498269651 — public, Fixed, CVE-2026-6312, “Cross-domain password leak via manual-fallback preview in PasswordManualFallbackFlow”; that is the preview path (DidSelectSuggestion), and its shipped fix refuses even a masked preview on every platform, which is why Android today refuses to show the credential while still handing over its plaintext.

The in-flight hardening in this file is a different check: kPasswordManualFallbackSecurityChecks (password_autofill_manager.cc:578-589, password_features.cc:178-179) is FEATURE_DISABLED_BY_DEFAULT, and its content is frame eligibility — HasValidURL() and IsRenderFrameHostSupported() — both of which an ordinary top-level frame serving the attacker’s own origin satisfies. It says nothing about whose credential is released.

Impact analysis

A compromised renderer walks away with the victim’s saved password for a different site, in plaintext.

Confidentiality. One tap yields one complete plaintext password belonging to another origin, delivered as the IPC payload itself — autofill_agent.mojom:192-197 FillPasswordSuggestionById(FieldRendererId, FieldRendererId, String16 username, String16 password, AutofillSuggestionTriggerSource) — so the renderer does not have to read it back out of the DOM at all. Measured: the 16-character Tr1cky-Fern-8823, saved for one origin, read by script running on another. The offered set is the entire saved-password store: on a newly registered attacker domain every stored credential is marked cross-domain (password_suggestion_generator.cc:664-667).

Integrity. None. The path reads stored credentials; it writes nothing to the victim’s profile.

Who is affected / Reach. Chrome for Android users of the built-in password manager with at least one saved password. No flag, origin trial, policy or permission prompt: the sources are compiled unconditionally (components/password_manager/core/browser/BUILD.gn:127-128) and the guard is unchanged on both branches serving Android Stable today, branch-heads/7977 (152.0.7977.x) and branch-heads/8010 (153.0.8010.27). The attacker needs a compromised renderer for a page they legitimately serve, plus one tap on the keyboard accessory — the ordinary way to fill a saved password on Android.

Bound. One tap yields one credential, and the list is not enumerable behind the user’s back: the accessory’s hover-preview path is behind kAutofillAndroidKeyboardAccessoryHoverPreview, disabled by default (autofill_features.cc:523-524), and previewing a cross-domain credential is refused on every platform (password_manual_fallback_flow.cc:231-238, :251-260). Android only: iOS hard-codes trigger_source = kiOS and password_request = std::nullopt at the entry point (autofill_driver_ios.mm:519-537), and a user on a third-party Android autofill provider gets a different client (autofill_client_provider.cc:59,74). Biometric re-auth is not a substitute and is not always present: on Android it requires kBiometricTouchToFill (password_features.cc:60, disabled by default) and the user’s own pref (chrome_password_manager_client.cc:756-759), and where it is on, the message passed to AuthenticateWithMessage is left empty on Android (password_manual_fallback_flow.cc:462-473) — the prompt confirms the device owner without naming whose credential is about to be released.

Severity. Chrome’s own documents state the boundary this crosses. content/SECURITY.md:82-83 lists, among High-Priority Vulnerabilities under Site Isolation Bypasses, “Access to Cross-Site Data: Allowing a compromised renderer to access cross-site data, such as cookies or passwords”, and docs/security/compromised-renderers.md:136-137 states “Compromised renderers shouldn’t be able to read or write passwords of other sites” — a document whose own preamble (:21-25) says newly discovered holes in that protection are considered security bugs. The decided precedent is the same policy in the same file, one switch arm away: https://issues.chromium.org/issues/498269651, CVE-2026-6312, fixed and merged to M147, M148 and the M144 LTS branch; that report was the masked preview of a cross-domain credential, and this one is its plaintext. The rubric’s mitigating factors do not apply: one tap on a password chip is the typical Android filling gesture rather than implausible interaction (severity-guidelines.md:24-26), and the rubric states that applying only to a particular group of users is not mitigating (:28-31).


The cause

What version of Chrome have you found the security issue in?

Google Chrome for Android 154.0.8024.0, a build made for this report from chromium/src ce7f1dd1ae9bcb33571f4bd9e0d3a8953c217491 with the two attached patches, measured 2026-08-26 on a Pixel 8 Pro running Android 17 (build CP2A.260805.005, security patch 2026-08-05); the supporting measurements on stock released binaries are Google Chrome for Android 151.0.7922.169 (Pixel 8 Pro) and 151.0.7922.171 (Pixel 8), measured 2026-08-25 on that same Android 17 build. Android Stable on 2026-09-04 is 152.0.7977.77 and 152.0.7977.82 (M152) and 153.0.8010.27 (M153, promoted 2026-09-03); the guard is present unchanged on both shipping branches, branch-heads/7977 and branch-heads/8010.

No, it is not related to a crash.

Choose the type of vulnerability

Site Isolation Bypass

How would you like to be publicly acknowledged for your report?

OGINOME Tomohito

View on issue tracker