Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Payments
DescriptionInsufficient validation of untrusted input in Payments
ComponentPayments
Bug ClassLogic Error
Tracker499099003
Fix commit2071ecba7491 (chromium/src) +30/-11
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
if
components/facilitated_payments/core/browser/payment_link_manager.cc
modified

Files Changed

  • components/facilitated_payments/core/browser/payment_link_manager.cc
  • components/facilitated_payments/core/browser/payment_link_manager_unittest.cc
From 2071ecba74911e854a6cb16918d6dcf7d11f54ad Mon Sep 17 00:00:00 2001
From: Siddharth <siashah@google.com>
Date: Sun, 05 Apr 2026 02:39:39 -0700
Subject: [PATCH] Do not update PaymentLinkManager state if in progress

Bug: 499099003
Change-Id: I551491bb697784c583291ae256c341e85b9e7360
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7729154
Reviewed-by: Junhui He <junhuihe@google.com>
Commit-Queue: Siddharth Shah <siashah@google.com>
Cr-Commit-Position: refs/heads/main@{#1610173}
---

diff --git a/components/facilitated_payments/core/browser/payment_link_manager.cc b/components/facilitated_payments/core/browser/payment_link_manager.cc
index a55fd61e..00db0b5 100644
--- a/components/facilitated_payments/core/browser/payment_link_manager.cc
+++ b/components/facilitated_payments/core/browser/payment_link_manager.cc
@@ -60,6 +60,9 @@
     const GURL& payment_link_url,
     const GURL& page_url,
     ukm::SourceId ukm_source_id) {
+  if (ui_state_ != UiState::kHidden) {
+    return;
+  }
   payment_flow_triggered_timestamp_ = base::TimeTicks::Now();
   ukm_source_id_ = ukm_source_id;
   LogPaymentLinkDetected(ukm_source_id_);
diff --git a/components/facilitated_payments/core/browser/payment_link_manager_unittest.cc b/components/facilitated_payments/core/browser/payment_link_manager_unittest.cc
index 46190228..019b649 100644
--- a/components/facilitated_payments/core/browser/payment_link_manager_unittest.cc
+++ b/components/facilitated_payments/core/browser/payment_link_manager_unittest.cc
@@ -859,6 +859,33 @@
       /*expected_bucket_count=*/1);
 }
 
+// Test that multiple calls to TriggerPaymentLinkPushPayment are ignored.
+TEST_F(PaymentLinkManagerTest,
+       TriggerPaymentLinkPushPayment_MultipleCalls_Ignored) {
+  GURL page_url("https://example.com/");
+  payments_data_manager_.AddEwalletForTest(autofill::Ewallet(
+      /*instrument_id=*/100, u"nickname",
+      /*display_icon_url=*/page_url, u"ewallet_name", u"account_display_name",
+      /*supported_payment_link_uris=*/
+      {u"^shopeepay:\\/\\/shopeepay\\.com\\.my\\?code=.*$",
+       u"^tngd:\\/\\/tngdigital\\.com\\.my\\?code=.*$"},
+      /*is_fido_enrolled=*/true));
+  GURL supported_payment_link(
+      "shopeepay://shopeepay.com.my?code=https://shopeepay.com.my/"
+      "281011051692389958586862838?merchant=Walmart&amount=101&currency=usd");
+
+  // ShowPaymentLinkPrompt should be called exactly once.
+  EXPECT_CALL(client_, ShowPaymentLinkPrompt).Times(1);
+
+  // First call should trigger the payment flow.
+  payment_link_manager_->TriggerPaymentLinkPushPayment(
+      supported_payment_link, page_url, ukm::UkmRecorder::GetNewSourceID());
+
+  // Second call should be ignored because the UI state is no longer hidden.
+  payment_link_manager_->TriggerPaymentLinkPushPayment(
+      supported_payment_link, page_url, ukm::UkmRecorder::GetNewSourceID());
+}
+
 // Test that eWalet payment prompt is shown for websites in the allowlist.
 TEST_F(PaymentLinkManagerTest,
        TriggerPaymentLinkPushPayment_UrlInAllowlist_EwalletPaymentPromptShown) {
@@ -1807,17 +1834,6 @@
   ON_CALL(*mock_facilitated_payments_app_info_list_, Size)
       .WillByDefault(testing::Return(2));
 
-  // Test that when `kFacilitatedPaymentsA2AEnabled` pref is true,
-  // `ShowPaymentLinkPrompt` is invoked.
-  pref_service_.get()->SetBoolean(
-      autofill::prefs::kFacilitatedPaymentsA2AEnabled, true);
-
-  EXPECT_CALL(client_, ShowPaymentLinkPrompt).Times(1);
-
-  payment_link_manager_->TriggerPaymentLinkPushPayment(
-      supported_payment_link, GURL("https://www.example.com"),
-      ukm::UkmRecorder::GetNewSourceID());
-
   // Test that when `kFacilitatedPaymentsA2AEnabled` pref is false,
   // `ShowPaymentLinkPrompt` is not invoked.
   pref_service_.get()->SetBoolean(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/facilitated_payments/core/browser/payment_link_manager_unittest.cc b/components/facilitated_payments/core/browser/payment_link_manager_unittest.cc
index 46190228..019b649 100644
--- a/components/facilitated_payments/core/browser/payment_link_manager_unittest.cc
+++ b/components/facilitated_payments/core/browser/payment_link_manager_unittest.cc
@@ -859,6 +859,33 @@
       /*expected_bucket_count=*/1);
 }
 
+// Test that multiple calls to TriggerPaymentLinkPushPayment are ignored.
+TEST_F(PaymentLinkManagerTest,
+       TriggerPaymentLinkPushPayment_MultipleCalls_Ignored) {
+  GURL page_url("https://example.com/");
+  payments_data_manager_.AddEwalletForTest(autofill::Ewallet(
+      /*instrument_id=*/100, u"nickname",
+      /*display_icon_url=*/page_url, u"ewallet_name", u"account_display_name",
+      /*supported_payment_link_uris=*/
+      {u"^shopeepay:\\/\\/shopeepay\\.com\\.my\\?code=.*$",
+       u"^tngd:\\/\\/tngdigital\\.com\\.my\\?code=.*$"},
+      /*is_fido_enrolled=*/true));
+  GURL supported_payment_link(
+      "shopeepay://shopeepay.com.my?code=https://shopeepay.com.my/"
+      "281011051692389958586862838?merchant=Walmart&amount=101&currency=usd");
+
+  // ShowPaymentLinkPrompt should be called exactly once.
+  EXPECT_CALL(client_, ShowPaymentLinkPrompt).Times(1);
+
+  // First call should trigger the payment flow.
+  payment_link_manager_->TriggerPaymentLinkPushPayment(
+      supported_payment_link, page_url, ukm::UkmRecorder::GetNewSourceID());
+
+  // Second call should be ignored because the UI state is no longer hidden.
+  payment_link_manager_->TriggerPaymentLinkPushPayment(
+      supported_payment_link, page_url, ukm::UkmRecorder::GetNewSourceID());
+}
+
 // Test that eWalet payment prompt is shown for websites in the allowlist.
 TEST_F(PaymentLinkManagerTest,
        TriggerPaymentLinkPushPayment_UrlInAllowlist_EwalletPaymentPromptShown) {
@@ -1807,17 +1834,6 @@
   ON_CALL(*mock_facilitated_payments_app_info_list_, Size)
       .WillByDefault(testing::Return(2));
 
-  // Test that when `kFacilitatedPaymentsA2AEnabled` pref is true,
-  // `ShowPaymentLinkPrompt` is invoked.
-  pref_service_.get()->SetBoolean(
-      autofill::prefs::kFacilitatedPaymentsA2AEnabled, true);
-
-  EXPECT_CALL(client_, ShowPaymentLinkPrompt).Times(1);
-
-  payment_link_manager_->TriggerPaymentLinkPushPayment(
-      supported_payment_link, GURL("https://www.example.com"),
-      ukm::UkmRecorder::GetNewSourceID());
-
   // Test that when `kFacilitatedPaymentsA2AEnabled` pref is false,
   // `ShowPaymentLinkPrompt` is not invoked.
   pref_service_.get()->SetBoolean(
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential payment URL swap in FacilitatedPayments due to missing re-entrancy guard

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 logic vulnerability exists in the Facilitated Payments component where PaymentLinkManager::TriggerPaymentLinkPushPayment lacks a browser-side re-entrancy guard. A compromised renderer can send multiple HandlePaymentLink Mojo IPCs to silently swap the underlying payment URL after the Chrome bottom sheet UI is presented. Because the initial Chrome UI does not display transaction details, the user may unknowingly authorize an attacker-controlled payment.

Affected files:

  • components/facilitated_payments/core/browser/payment_link_manager.cc
  • components/facilitated_payments/content/browser/content_facilitated_payments_driver.cc
  • chrome/browser/facilitated_payments/ui/android/facilitated_payments_controller.cc

Estimated timestamp from git blame: 2025-10-01

Summary

A logic vulnerability exists in the browser process where PaymentLinkManager::TriggerPaymentLinkPushPayment does not check for an active payment flow before initializing state. By rapidly triggering the HandlePaymentLink Mojo endpoint twice, a compromised renderer can display a payment prompt for a legitimate transaction, and immediately swap the underlying transaction state and UI callbacks to a malicious URL.

Because the initial Chrome Android bottom sheet only displays payment methods and omits transaction details (like amount and recipient), the user is unaware of the swap.

Vulnerability Details

Unlike PixManager::OnPixCodeCopiedToClipboard, which utilizes a has_payflow_started_ boolean to prevent concurrent flows, PaymentLinkManager::TriggerPaymentLinkPushPayment unconditionally resets its state every time it is called:

// components/facilitated_payments/core/browser/payment_link_manager.cc
86:   initiate_payment_request_details_ =
87:       std::make_unique<FacilitatedPaymentsInitiatePaymentRequestDetails>();
... 
90:   initiate_payment_request_details_->payment_link_ = payment_link_url.spec();

If a compromised renderer sends two sequential HandlePaymentLink Mojo IPCs:

  1. The first call initializes the state with a legitimate URL and shows the Chrome bottom sheet via FacilitatedPaymentsController::ShowForPaymentLink.
  2. The second call executes synchronously. It replaces initiate_payment_request_details_ with the malicious URL and triggers the UI display again.

In FacilitatedPaymentsController::ShowForPaymentLink, the new callback simply overwrites the old one:

// chrome/browser/facilitated_payments/ui/android/facilitated_payments_controller.cc
65:   on_fop_selected_ = std::move(on_fop_selected);

The Android UI detects the sheet is already open and swaps the view seamlessly in-place. When the user taps a payment method, the new callback executes, writing the selected instrument ID into the swapped initiate_payment_request_details_ and sending the attacker’s URL to the Google Pay backend.

Suggested Steps to Reproduce

Note: These are potential steps to reproduce the issue. Our tooling agent does not yet have the ability to run code or execute a working proof of concept.

  1. An attacker compromises the renderer of an origin on the Facilitated Payments allowlist.
  2. The compromised renderer invokes the HandlePaymentLink Mojo endpoint with a legitimate, low-value URL (e.g., $1.00).
  3. Chrome displays the payment method selection bottom sheet.
  4. Before the user interacts, the renderer invokes HandlePaymentLink again with a high-value, attacker-controlled URL sharing the same scheme.
  5. Chrome synchronously overwrites the browser-side transaction state and UI callback. The Android UI updates in place unnoticeably.
  6. The user selects a payment method, believing they are authorizing the $1.00 transaction.
  7. Chrome requests a transaction token from the backend for the malicious URL.
  8. The user is presented with the final Google Play Services prompt. Although this prompt displays the correct (malicious) amount and recipient, the user is highly habituated to click “Confirm” immediately, as they just initiated a transaction in the browser.

Suggested Fix

Introduce a state check in PaymentLinkManager::TriggerPaymentLinkPushPayment to act as a re-entrancy guard, similar to PixManager.

For example, add a has_payflow_started_ boolean flag that is checked at the top of the function. If true, the method should return early or explicitly cancel/reset the ongoing flow before initializing the new request. This ensures the UI state and underlying network request details remain perfectly synchronized.

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