Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation Passwords
DescriptionInappropriate implementation Passwords
ComponentChromium
Bug ClassLogic Error
Tracker518128953
Fix commitabbd9754bec8 (chromium/src) +88/-26
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-11

Changed Functions

FunctionChangeNotes
if
chrome/browser/password_manager/chrome_password_manager_client.cc
modified
if
chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.cc
modified
PasskeyCredential
chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.h
modified

Files Changed

  • chrome/browser/password_manager/chrome_password_manager_client.cc
  • chrome/browser/password_manager/chrome_password_manager_client.h
  • chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc
  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.cc
  • chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.h
From abbd9754bec812bb36ba6d261b15fdc860cb2e75 Mon Sep 17 00:00:00 2001
From: Mohamed Amir Yosef <[email protected]>
Date: Mon, 08 Jun 2026 08:27:22 -0700
Subject: [PATCH] [PasswordManager] Use committed origin for TouchToFill

This CL updates the Android TouchToFill (TTF) credential cache lookup in
ContinueShowKeyboardReplacingSurface to query saved credentials using
the frame's actual committed origin instead of converting its URL.

Previously, the lookup used URLToOrigin(driver->GetLastCommittedURL()),
which stripped the opaque origin wrapper of sandboxed same-site
iframes (e.g. converting https://victim.com/ugc.html in a sandboxed
iframe to the tuple origin https://victim.com). This precursor origin
confusion allowed process-isolated sandboxed renderers to retrieve and
display credentials belonging to their parent origin.

Using the committed origin (via driver->GetLastCommittedOrigin())
correctly preserves sandboxed iframe boundaries since an opaque origin
lookup will fail to match the non-opaque parent origin in the cache,
safely returning an empty credential store.

Additionally, this CL refactors MarkSharedCredentialsAsNotified and
its related interfaces (PasswordCredentialFiller,
TouchToFillControllerDelegate) to use url::Origin instead of GURL,
preventing precursor origin confusion when marking credentials as
notified.

Fixed: 518128953
Test: components_unittests --gtest_filter="PasswordCredentialFiller*"
Test: unit_tests --gtest_filter="*ChromePasswordManagerClient*"
Change-Id: I96a20fd2062a598c53f7d2046972247824782c81
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7902320
Commit-Queue: Mohamed Amir Yosef <[email protected]>
Reviewed-by: Maria Kazinova <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1643221}
---

diff --git a/chrome/browser/password_manager/chrome_password_manager_client.cc b/chrome/browser/password_manager/chrome_password_manager_client.cc
index 25caf59..62d55a0 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client.cc
@@ -225,12 +225,6 @@
 constexpr char kPasswordBreachEntryTrigger[] = "PASSWORD_ENTRY";
 #endif
 
-#if BUILDFLAG(IS_ANDROID)
-// TODO(crbug.com/41485955): Get rid of DeprecatedGetOriginAsURL().
-url::Origin URLToOrigin(GURL url) {
-  return url::Origin::Create(url.DeprecatedGetOriginAsURL());
-}
-#endif  // BUILDFLAG(IS_ANDROID)
 
 }  // namespace
 
@@ -635,6 +629,9 @@
   // without being called.
   auto split_delay_callback =
       base::SplitOnceCallback(std::move(delay_callback));
+  if (!weak_driver) {
+    return;
+  }
   password_manager::ContentPasswordManagerDriver* driver =
       static_cast<password_manager::ContentPasswordManagerDriver*>(
           weak_driver.get());
@@ -684,8 +681,7 @@
               should_show_hybrid_option));
 
   base::span<const password_manager::UiCredential> password_credentials =
-      credential_cache_
-          .GetCredentialStore(URLToOrigin(driver->GetLastCommittedURL()))
+      credential_cache_.GetCredentialStore(driver->GetLastCommittedOrigin())
           .GetCredentials();
   std::vector<TouchToFillView::Credential> credentials;
   credentials.reserve(password_credentials.size() + passkeys.size());
@@ -1405,10 +1401,9 @@
 }
 
 void ChromePasswordManagerClient::MarkSharedCredentialsAsNotified(
-    const GURL& url) {
-  for (const PasswordForm& form :
-       credential_cache_.GetCredentialStore(URLToOrigin(url))
-           .GetUnnotifiedSharedCredentials()) {
+    const url::Origin& origin) {
+  for (const PasswordForm& form : credential_cache_.GetCredentialStore(origin)
+                                      .GetUnnotifiedSharedCredentials()) {
     // Make a non-const copy so we can modify it.
     password_manager::PasswordForm updatedForm = form;
     updatedForm.sharing_notification_displayed = true;
diff --git a/chrome/browser/password_manager/chrome_password_manager_client.h b/chrome/browser/password_manager/chrome_password_manager_client.h
index db965a5..42d295b 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client.h
+++ b/chrome/browser/password_manager/chrome_password_manager_client.h
@@ -320,7 +320,7 @@
 #if BUILDFLAG(IS_ANDROID)
   webauthn::WebAuthnCredManDelegate* GetWebAuthnCredManDelegateForDriver(
       password_manager::PasswordManagerDriver* driver) override;
-  void MarkSharedCredentialsAsNotified(const GURL& url) override;
+  void MarkSharedCredentialsAsNotified(const url::Origin& origin) override;
 #endif  // BUILDFLAG(IS_ANDROID)
   version_info::Channel GetChannel() const override;
   void RefreshPasswordManagerSettingsIfNeeded() const override;
diff --git a/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc b/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
index ca8e71cd..594b0148 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
@@ -1998,7 +1998,7 @@
   EXPECT_CALL(*account_store, UpdateLogin(password_manager::EqStoredCredential(
                                               shared_not_notified_account),
                                           _));
-  GetClient()->MarkSharedCredentialsAsNotified(kURL);
+  GetClient()->MarkSharedCredentialsAsNotified(origin);
 }
 
 #endif  //  BUILDFLAG(IS_ANDROID)
diff --git a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc
index d9c86c32..af3aa93 100644
--- a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc
+++ b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc
@@ -110,6 +110,7 @@
   // If the render frame host has been destroyed already, the url will be empty
   // in which case Show() should never be called.
   CHECK(!url.is_empty());
+  url::Origin origin = ttf_delegate_->GetFrameOrigin();
 
   switch (GetResponsibleDisplayTarget(credentials_)) {
     case DisplayTarget::kNone:
@@ -127,7 +128,7 @@
         no_passkeys_bridge_ = std::make_unique<NoPasskeysBottomSheetBridge>();
       }
       no_passkeys_bridge_->Show(
-          GetNativeView()->GetWindowAndroid(), url::Origin::Create(url).host(),
+          GetNativeView()->GetWindowAndroid(), origin.host(),
           base::BindOnce(&TouchToFillController::OnDismiss,
                          weak_ptr_factory_.GetWeakPtr()),
           base::BindOnce(&TouchToFillController::OnHybridSignInSelected,
@@ -169,8 +170,7 @@
 
       return view_->Show(url,
                          TouchToFillView::IsOriginSecure(
-                             network::IsOriginPotentiallyTrustworthy(
-                                 url::Origin::Create(url))),
+                             network::IsOriginPotentiallyTrustworthy(origin)),
                          *sorted_credentials, flags);
   }
 }
@@ -182,7 +182,7 @@
   if (credential.match_type() ==
       password_manager_util::GetLoginMatchType::kGrouped) {
     std::string current_origin =
-        GetDisplayOrigin(url::Origin::Create(ttf_delegate_->GetFrameUrl()));
+        GetDisplayOrigin(ttf_delegate_->GetFrameOrigin());
     // Use `cred->display_name()` instead of origin here to correctly display
     // credentials saved for android apps.
     grouped_credential_sheet_controller_->ShowAcknowledgeSheet(
diff --git a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.cc b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.cc
index 139b0bb..44de876 100644
--- a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.cc
+++ b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.cc
@@ -231,6 +231,11 @@
   return filler_->GetFrameUrl();
 }
 
+url::Origin TouchToFillControllerAutofillDelegate::GetFrameOrigin() {
+  CHECK(filler_);
+  return filler_->GetFrameOrigin();
+}
+
 bool TouchToFillControllerAutofillDelegate::ShouldShowTouchToFill() {
   if (!form_to_fill_) {
     return false;
@@ -336,9 +341,9 @@
   // this case it's not possible to mark credentials as notitied. If the user
   // has properly interact with the touch to fill UI, the client would have been
   // notified properly.
-  GURL url = GetFrameUrl();
-  if (!url.is_empty()) {
-    password_client_->MarkSharedCredentialsAsNotified(url);
+  url::Origin origin = GetFrameOrigin();
+  if (!origin.opaque()) {
+    password_client_->MarkSharedCredentialsAsNotified(origin);
   }
   filler_.reset();
   base::UmaHistogramEnumeration("PasswordManager.TouchToFill.Outcome", outcome);
diff --git a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.h b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.h
index 46bd346c..c9991e0 100644
--- a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.h
+++ b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.h
@@ -25,6 +25,7 @@
 #include "components/password_manager/core/browser/password_manager_metrics_util.h"
 #include "services/metrics/public/cpp/ukm_source_id.h"
 #include "ui/gfx/native_ui_types.h"
+#include "url/origin.h"
 
 namespace password_manager {
 class PasskeyCredential;
@@ -127,6 +128,7 @@
   void OnDismiss(base::OnceClosure action_completed) override;
   void OnCredManDismissed(base::OnceClosure action_completed) override;
   GURL GetFrameUrl() override;
+  url::Origin GetFrameOrigin() override;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc b/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
index ca8e71cd..594b0148 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
@@ -1998,7 +1998,7 @@
   EXPECT_CALL(*account_store, UpdateLogin(password_manager::EqStoredCredential(
                                               shared_not_notified_account),
                                           _));
-  GetClient()->MarkSharedCredentialsAsNotified(kURL);
+  GetClient()->MarkSharedCredentialsAsNotified(origin);
 }
 
 #endif  //  BUILDFLAG(IS_ANDROID)
diff --git a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate_unittest.cc b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate_unittest.cc
index c10ebbe..4b62dad 100644
--- a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate_unittest.cc
+++ b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate_unittest.cc
@@ -87,7 +87,10 @@
               GetWebAuthnCredentialsDelegateForDriver,
               (password_manager::PasswordManagerDriver*),
               (override));
-  MOCK_METHOD(void, MarkSharedCredentialsAsNotified, (const GURL&), (override));
+  MOCK_METHOD(void,
+              MarkSharedCredentialsAsNotified,
+              (const url::Origin&),
+              (override));
   MOCK_METHOD(bool,
               IsReauthBeforeFillingRequired,
               (device_reauth::DeviceAuthenticator*),
@@ -145,6 +148,8 @@
     // cache the raw pointer here to interact with the mock after passing.
     weak_filler_ = filler.get();
     ON_CALL(*filler, GetFrameUrl()).WillByDefault(Return(GURL(kExampleCom)));
+    ON_CALL(*filler, GetFrameOrigin())
+        .WillByDefault(Return(url::Origin::Create(GURL(kExampleCom))));
     return filler;
   }
 
@@ -486,6 +491,8 @@
   auto filler_to_pass = CreateMockFiller();
   EXPECT_CALL(*last_mock_filler(), GetFrameUrl())
       .WillOnce(Return(GURL("http://example.com")));
+  EXPECT_CALL(*last_mock_filler(), GetFrameOrigin())
+      .WillOnce(Return(url::Origin::Create(GURL("http://example.com"))));
 
   Credential credentials[] = {
       MakeUiCredential({.username = "alice", .password = "p4ssw0rd"})};
@@ -615,7 +622,8 @@
            TouchToFillControllerAutofillDelegate::ShowHybridOption(false)),
        /*cred_man_delegate=*/nullptr);
 
-  EXPECT_CALL(client(), MarkSharedCredentialsAsNotified(GURL(kExampleCom)));
+  EXPECT_CALL(client(), MarkSharedCredentialsAsNotified(
+                            url::Origin::Create(GURL(kExampleCom))));
   touch_to_fill_controller().OnDismiss();
 
   auto entries = test_recorder().GetEntriesByName(UkmBuilder::kEntryName);
@@ -644,7 +652,8 @@
            TouchToFillControllerAutofillDelegate::ShowHybridOption(false)),
        /*cred_man_delegate=*/nullptr);
 
-  EXPECT_CALL(client(), MarkSharedCredentialsAsNotified(GURL(kExampleCom)));
+  EXPECT_CALL(client(), MarkSharedCredentialsAsNotified(
+                            url::Origin::Create(GURL(kExampleCom))));
   EXPECT_CALL(client(),
               NavigateToManagePasswordsPage(
                   password_manager::ManagePasswordsReferrer::kTouchToFill));
diff --git a/components/password_manager/core/browser/password_credential_filler_impl_unittest.cc b/components/password_manager/core/browser/password_credential_filler_impl_unittest.cc
index 257ae98..5d34d321 100644
--- a/components/password_manager/core/browser/password_credential_filler_impl_unittest.cc
+++ b/components/password_manager/core/browser/password_credential_filler_impl_unittest.cc
@@ -21,6 +21,7 @@
 #include "components/password_manager/core/common/password_manager_features.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "url/origin.h"
 
 namespace {
 
@@ -44,6 +45,7 @@
               (override));
   MOCK_METHOD(void, TriggerFormSubmission, (), (override));
   MOCK_METHOD(const GURL&, GetLastCommittedURL, (), (const override));
+  MOCK_METHOD(const url::Origin&, GetLastCommittedOrigin, (), (const override));
 };
 
 enum class FormFieldFocusabilityType {
@@ -149,6 +151,8 @@
   void SetUp() override {
     ON_CALL(driver_, GetLastCommittedURL())
         .WillByDefault(ReturnRefOfCopy(GURL(kExampleCom)));
+    ON_CALL(driver_, GetLastCommittedOrigin())
+        .WillByDefault(ReturnRefOfCopy(url::Origin::Create(GURL(kExampleCom))));
   }
 
   MockPasswordManagerDriver& driver() { return driver_; }
@@ -226,6 +230,23 @@
   filler.FillUsernameAndPassword(kUsername, kPassword, base::DoNothing());
 }
 
+TEST_F(PasswordCredentialFillerBaseTest, GetFrameOrigin) {
+  PasswordCredentialFillerImpl filler(
+      driver().AsWeakPtr(),
+      CreatePasswordSuggestionRequest({}, /*has_captcha=*/false,
+                                      /*username_field_index=*/0,
+                                      /*password_field_index=*/0));
+  EXPECT_EQ(filler.GetFrameOrigin(), url::Origin::Create(GURL(kExampleCom)));
+}
+
+TEST_F(PasswordCredentialFillerBaseTest, GetFrameOriginWithNullDriver) {
+  PasswordCredentialFillerImpl filler(
+      nullptr, CreatePasswordSuggestionRequest({}, /*has_captcha=*/false,
+                                               /*username_field_index=*/0,
+                                               /*password_field_index=*/0));
+  EXPECT_TRUE(filler.GetFrameOrigin().opaque());
+}
+
 class PasswordCredentialFillerV2ParameterTest
     : public PasswordCredentialFillerBaseTest,
       public testing::WithParamInterface<
Loading diff…

Original Bug Report

reported by [email protected]

Potential same-site sandboxed iframe credential leak in TouchToFill via precursor origin confusion

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential vulnerability in Android’s TouchToFill password suggestions allows a compromised same-site sandboxed iframe to bypass security boundaries and access parent origin credentials. The issue stems from precursor origin confusion where the browser queries saved credentials using a precursor-derived tuple origin instead of the frame’s actual committed origin. This can allow a process-isolated sandboxed renderer to trigger TouchToFill and obtain the parent’s plaintext credentials.

Affected files:

  • chrome/browser/password_manager/chrome_password_manager_client.cc
  • components/password_manager/core/browser/password_manager.cc

Estimated timestamp from git blame: 2019-07-31

Root Cause Analysis

In chrome/browser/password_manager/chrome_password_manager_client.cc inside the Android-only ContinueShowKeyboardReplacingSurface function (which handles TouchToFill), the code retrieves credentials from the cache using a precursor-derived origin of the document’s URL rather than the driver frame’s actual committed origin:

// chrome/browser/password_manager/chrome_password_manager_client.cc:684-687
base::span<const password_manager::UiCredential> password_credentials =
    credential_cache_
        .GetCredentialStore(URLToOrigin(driver->GetLastCommittedURL()))
        .GetCredentials();

Here, URLToOrigin is implemented as:

// chrome/browser/password_manager/chrome_password_manager_client.cc:228-230
url::Origin URLToOrigin(GURL url) {
  return url::Origin::Create(url.DeprecatedGetOriginAsURL());
}

DeprecatedGetOriginAsURL() strips path components and creates a tuple origin (e.g., https://victim.com), discarding the opaque origin wrapper associated with sandboxed frames. This leads to precursor origin confusion: a sandboxed iframe with an opaque origin is treated as having its parent’s non-opaque tuple origin, matching the credential key populated by the main frame.

To display suggestions, TouchToFill requires a valid PasswordFormManager to be registered for the frame’s driver. Normally, a sandboxed frame is prevented from initializing a PasswordFormManager because standard renderer-driven IPC messages (such as PasswordFormsParsed or PasswordFormsRendered) undergo security validation in ContentPasswordManagerDriver via HasValidURL(), which checks process capability under IsAccessAllowedForSandboxedProcess and drops the messages.

However, the browser’s asynchronous server prediction path bypasses this mechanism. When the browser receives Autofill server predictions for the sandboxed iframe’s form, ChromePasswordManagerClient::PropagatePredictionsToPasswordManager is called on the browser side. This invokes PasswordManager::ProcessAutofillPredictions (in components/password_manager/core/browser/password_manager.cc lines 1777–1844), which is entirely browser-initiated and does not perform HasValidURL() or ChildProcessSecurityPolicy validation. If the server predictions classify the form as a potential credential or username-first flow, ProcessAutofillPredictions invokes CreateFormManager to instantiate and cache a PasswordFormManager for the sandboxed iframe’s driver.

When the sandboxed frame subsequently triggers a suggestion request, the browser successfully retrieves the main frame’s cached credentials due to precursor origin confusion. If the user selects a credential, driver_->FillSuggestion transmits the plaintext username and password straight to the process-isolated sandboxed renderer process.


Potential Steps to Trigger / Exploit Flow

Note: These are suggested/potential steps as our tooling agent does not have the ability to execute code to verify a functional proof of concept.

  1. A user visits a target page https://victim.com on an Android device with partial-site-isolation enabled. The user has saved credentials for https://victim.com cached in credential_cache_ via the main frame.
  2. The main page embeds an attacker-controlled same-site sandboxed iframe: <iframe sandbox="allow-scripts" src="https://victim.com/ugc.html">.
  3. The sandboxed iframe is isolated in its own dedicated sandboxed renderer process under kIsolateSandboxedIframes due to its opaque origin.
  4. An attacker with a renderer compromise primitive (Renderer RCE) in the sandboxed process renders a login-like form and focuses the field.
  5. The browser-side Autofill engine processes predictions for the form and asynchronously triggers ProcessAutofillPredictions in PasswordManager, registering a PasswordFormManager for the sandboxed iframe’s driver and bypassing HasValidURL() checks.
  6. The compromised renderer sends an AskForValuesToFill Mojo request. TouchToFill displays the parent’s credentials because URLToOrigin(driver->GetLastCommittedURL()) matches https://victim.com instead of evaluating to an opaque origin.
  7. Once the user taps the credential entry, driver_->FillSuggestion is executed, sending the plaintext username and password across the process boundary to the compromised sandboxed renderer process.

Suggested Fix

To prevent precursor origin confusion and preserve sandboxed iframe boundaries, the credential cache lookups in ContinueShowKeyboardReplacingSurface should use the frame’s committed origin instead of converting its URL.

Update chrome/browser/password_manager/chrome_password_manager_client.cc to query the credential store using the driver’s last committed origin:

base::span<const password_manager::UiCredential> password_credentials =
    credential_cache_
        .GetCredentialStore(driver->GetLastCommittedOrigin())
        .GetCredentials();

Because a sandboxed iframe’s committed origin is opaque, this lookup will safely fail to match the non-opaque https://victim.com key in credential_cache_, preventing any credentials from being retrieved or leaked.

Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040


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.

View on issue tracker