Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Payments
DescriptionInappropriate implementation in Payments
ComponentPayments
Bug ClassLogic Error
Tracker497344640
Fix commitb5b9a3cf7cf8 (chromium/src) +21/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactory.java
  • chrome/browser/payments/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactoryTest.java
From b5b9a3cf7cf81e959730d08e6443fd8d1ae98792 Mon Sep 17 00:00:00 2001
From: Luis Antunes <luisantunes@google.com>
Date: Wed, 15 Apr 2026 08:16:22 -0700
Subject: [PATCH] Ensure RenderFrameHost is active in ChromePaymentsRequestFactory.java.

RenderFrameHost could have been removed (e.g. page was navigated away)
when ChromePaymentRequestFactoryTest is trying to create the
PaymentRequestService.

Bug: 497344640
Change-Id: I39e0dacfbf8c4320f67ba5ae7c893d3c6230634a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7758421
Reviewed-by: Slobodan Pejic <slobodan@chromium.org>
Reviewed-by: Darwin Yang <darwinyang@chromium.org>
Commit-Queue: Luis Antunes <luisantunes@google.com>
Cr-Commit-Position: refs/heads/main@{#1615185}
---

diff --git a/chrome/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactory.java b/chrome/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactory.java
index 3a2d6cee..b7f507f 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactory.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactory.java
@@ -24,6 +24,7 @@
 import org.chromium.components.payments.PaymentRequestServiceUtil;
 import org.chromium.components.payments.SslValidityChecker;
 import org.chromium.components.user_prefs.UserPrefs;
+import org.chromium.content_public.browser.LifecycleState;
 import org.chromium.content_public.browser.PermissionsPolicyFeature;
 import org.chromium.content_public.browser.RenderFrameHost;
 import org.chromium.content_public.browser.WebContents;
@@ -133,7 +134,14 @@
 
     @Override
     public @Nullable PaymentRequest createImpl() {
-        if (mRenderFrameHost == null) return new InvalidPaymentRequest();
+        if (mRenderFrameHost == null
+                || mRenderFrameHost.getLifecycleState() != LifecycleState.ACTIVE) {
+            // This happens when the page has navigated away, which would cause the
+            // blink PaymentRequest to be released shortly, or when the iframe is being
+            // removed from the page.
+            return new InvalidPaymentRequest();
+        }
+
         if (!mRenderFrameHost.isFeatureEnabled(PermissionsPolicyFeature.PAYMENT)) {
             mRenderFrameHost.terminateRendererDueToBadMessage(241 /*PAYMENTS_WITHOUT_PERMISSION*/);
             return null;
diff --git a/chrome/browser/payments/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactoryTest.java b/chrome/browser/payments/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactoryTest.java
index cf6c0f2..5320a12 100644
--- a/chrome/browser/payments/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactoryTest.java
+++ b/chrome/browser/payments/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactoryTest.java
@@ -23,6 +23,7 @@
 import org.chromium.components.payments.InvalidPaymentRequest;
 import org.chromium.components.payments.PaymentFeatureList;
 import org.chromium.components.payments.test_support.DefaultPaymentFeatureConfig;
+import org.chromium.content_public.browser.LifecycleState;
 import org.chromium.content_public.browser.PermissionsPolicyFeature;
 import org.chromium.content_public.browser.RenderFrameHost;
 import org.chromium.content_public.browser.WebContents;
@@ -50,6 +51,8 @@
         Mockito.doReturn(true).when(mProfile).isOffTheRecord();
         Profile.setProfileFromWebContentsForTesting(mProfile);
 
+        Mockito.when(mRenderFrameHost.getLifecycleState()).thenReturn(LifecycleState.ACTIVE);
+
         setPaymentPermissionsPolicy(true);
     }
 
@@ -94,6 +97,15 @@
 
     @Test
     @Feature({"Payments"})
+    public void testInactiveLifecycleStateCausesInvalidPaymentRequest() {
+        Mockito.when(mRenderFrameHost.getLifecycleState())
+                .thenReturn(LifecycleState.IN_BACK_FORWARD_CACHE);
+        Assert.assertTrue(
+                createFactory(mRenderFrameHost).createImpl() instanceof InvalidPaymentRequest);
+    }
+
+    @Test
+    @Feature({"Payments"})
     @DisableFeatures(PaymentFeatureList.WEB_PAYMENTS)
     public void testDisabledFeatureCausesInvalidPaymentRequest() {
         Assert.assertTrue(
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Origin Spoofing in Android PaymentRequest via BFCached RenderFrameHost

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised renderer can bind the PaymentRequest Mojo interface after being placed into the Back/Forward Cache (BFCache) on Android. By initiating a payment from the BFCache after the tab navigates to a victim site, the attacker can spoof the payment origin and certificate, deceiving Android payment apps.

Affected files:

  • chrome/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactory.java
  • components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.java
  • chrome/android/java/src/org/chromium/chrome/browser/payments/ui/PaymentUiService.java
  • components/payments/content/android/java/src/org/chromium/components/payments/intent/WebPaymentIntentHelper.java

Estimated timestamp from git blame: 2025-08-11

Description

There is a potential UI spoofing and context hijack vulnerability in Chrome for Android’s WebPayments implementation.

The Android ChromePaymentRequestFactory.createImpl() method lacks a lifecycle state check before binding the payments.mojom.PaymentRequest interface. In contrast, the desktop C++ implementation (payment_request_factory.cc) explicitly checks render_frame_host->IsActive(), and Android WebView’s AwPaymentRequestFactory.java checks mRenderFrameHost.getLifecycleState() == LifecycleState.ACTIVE. Because BrowserInterfaceBroker does not automatically block non-associated Mojo interface requests from BFCached frames, a compromised renderer can bind PaymentRequest even when it is supposed to be frozen in the BFCache.

Once bound, PaymentRequestService.init() incorrectly derives security-critical fields (like mTopLevelOrigin and mMerchantName) from the WebContents associated with the RenderFrameHost via mWebContents.getLastCommittedUrl() and mWebContents.getTitle(). If the tab has navigated, these fields reflect the currently active page (the victim) rather than the BFCached page (the attacker).

The attacker can then call PaymentRequest.Show with had_user_activation=true. The browser trusts this renderer-provided flag, bypassing user activation checks, and passes the spoofed victim data to Android payment apps (e.g., Google Pay) via Intent extras (EXTRA_TOP_ORIGIN, EXTRA_MERCHANT_NAME).

Potential Trigger Steps

Note: Our tooling agent cannot run code, so these are proposed steps for an attacker with a compromised renderer.

  1. An attacker sets up https://attacker.example and compromises the renderer process hosting it.
  2. The compromised renderer navigates the tab to https://victim.example (e.g., a banking site).
  3. https://victim.example commits and becomes the active RenderFrameHost. https://attacker.example is moved into the BFCache (LifecycleState.kInBackForwardCache).
  4. Ignoring the BFCache freeze signal, the compromised renderer sends a Mojo GetInterface request for payments.mojom.PaymentRequest via its BrowserInterfaceBroker.
  5. ChromePaymentRequestFactory.createImpl() binds the interface because it omits the LifecycleState check.
  6. The attacker sends a PaymentRequest.Init IPC. PaymentRequestService.init() reads the tab’s current WebContents, fetching the victim’s URL and title for mTopLevelOrigin and mMerchantName.
  7. The attacker sends a PaymentRequest.Show(..., had_user_activation=true) IPC.
  8. The Android payment flow initiates, passing the spoofed EXTRA_TOP_ORIGIN to external Android payment apps, deceiving the user into authorizing a payment seemingly requested by victim.example.

Suggested Fix

  1. Update ChromePaymentRequestFactory.createImpl() in chrome/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactory.java to check the RenderFrameHost lifecycle state before allowing the binding, similar to AwPaymentRequestFactory:
if (mRenderFrameHost == null 
        || !mRenderFrameHost.isRenderFrameLive()
        || mRenderFrameHost.getLifecycleState() != LifecycleState.ACTIVE) {
    return new InvalidPaymentRequest();
}
  1. As a defense-in-depth measure, PaymentRequestService should fetch the origin and title from the RenderFrameHost that initiated the request, rather than relying on the tab-level WebContents.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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