Medium watch CVSS 4.2 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.2
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Payments
DescriptionInsufficient validation of untrusted input in Payments
ComponentPayments
Bug ClassInput Validation
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.
Tracker499099003
Fix commit2071ecba7491 (chromium/src) +30/-11
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-04-03 (per vendor advisory)
Fix landed2026-04-05 (public git)
Disclosed2026-05-05
Patch gap30 days — fix public in source before the advisory

Background

`PaymentLinkManager`
the facilitated-payments component that, on detecting a supported payment link on a page, drives the push-payment flow (eWallet/A2A) and shows the payment prompt UI.
`ui_state_` / `UiState::kHidden`
a member tracking the manager’s UI state machine; kHidden means no payment prompt is currently active, i.e. the manager is idle.
`TriggerPaymentLinkPushPayment`
the entry point (matching the hunk’s payment_link_url, page_url, ukm_source_id parameters) that starts a payment flow: it stamps payment_flow_triggered_timestamp_, sets ukm_source_id_, logs detection, and ultimately calls ShowPaymentLinkPrompt.
Payment flow state
payment_flow_triggered_timestamp_ and ukm_source_id_ are per-flow fields that must correspond to the single in-progress flow.

Root Cause Analysis

TriggerPaymentLinkPushPayment unconditionally (re)initialized the manager’s flow state — resetting payment_flow_triggered_timestamp_, overwriting ukm_source_id_, re-logging detection, and advancing toward ShowPaymentLinkPrompt — every time it was called, with no check that a flow was already underway. Because this method is reachable from page-observed payment links, a page could invoke it repeatedly, re-entering the flow and clobbering the state of an already-active payment prompt (the removed test comment and new MultipleCalls_Ignored test confirm a second call previously re-invoked ShowPaymentLinkPrompt).

The fix adds a guard at the top of the function — if (ui_state_ != UiState::kHidden) { return; } — so the flow only initializes when the manager is idle; any call while a prompt is already showing or a flow is in progress is dropped. This restores the invariant that flow state and the ShowPaymentLinkPrompt call happen exactly once per flow.

Key insight
The core mistake was letting TriggerPaymentLinkPushPayment reset and re-drive payment flow state while a flow was already active; the fix early-returns unless ui_state_ == UiState::kHidden, enforcing one trigger per flow.

Attack Path

  1. Serve a supported payment link A page hosts a payment link matching a registered eWallet’s supported_payment_link_uris, causing TriggerPaymentLinkPushPayment to fire and start the flow (setting state, showing the prompt).
  2. Re-trigger while in progress Before the flow completes, the page induces TriggerPaymentLinkPushPayment to be called again (per the regression test’s two back-to-back calls).
  3. Clobber active flow state Pre-patch, the second call resets payment_flow_triggered_timestamp_/ukm_source_id_ and re-invokes ShowPaymentLinkPrompt, corrupting the state of the already-active prompt.
  4. Consequence This yields duplicate/inconsistent prompt state driven by untrusted page input; the diff does not demonstrate a memory-safety primitive, so impact beyond state confusion is (inferred).

Impact Assessment

A malicious or scripted page can issue repeated payment-link triggers to re-enter and corrupt the PaymentLinkManager flow state while a payment prompt is active, matching the advisory’s Insufficient validation of untrusted input in Payments at medium severity. The code runs in the browser-side facilitated-payments component and requires a supported payment link (and the relevant eWallet/A2A configuration) to reach. The advisory does not report in-the-wild exploitation, and any escalation beyond flow-state confusion is (inferred).

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

Audit Directions

  • Idempotency of flow entry points
    audit state-machine “start”/“trigger” methods for a guard that rejects re-entry when the machine is not in its idle state, especially those reachable from renderer/page input.
  • Page-driven re-triggering
    review other facilitated-payments and prompt-showing paths (ShowPaymentLinkPrompt callers) for assumptions that they run once per user gesture/flow.
  • State-reset before completion
    flag methods that unconditionally overwrite per-flow fields (timestamps, source IDs, UI state) without checking whether a prior flow is still in progress.
From 2071ecba74911e854a6cb16918d6dcf7d11f54ad Mon Sep 17 00:00:00 2001
From: Siddharth <[email protected]>
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 <[email protected]>
Commit-Queue: Siddharth Shah <[email protected]>
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 [email protected]

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