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
Tracker500472958
Fix commit347b1a2e80de (chromium/src) +92/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.java
  • components/payments/content/android/junit/src/org/chromium/components/payments/PaymentRequestServiceTest.java
From 347b1a2e80de5da35f0ab0bd991014515691d261 Mon Sep 17 00:00:00 2001
From: Stephen McGruer <smcgruer@chromium.org>
Date: Mon, 08 Jun 2026 06:33:42 -0700
Subject: [PATCH] [Payments] Verify URL origin in openPaymentHandlerWindow

Before this change, PaymentRequestService.openPaymentHandlerWindow
blindly attached the requested GURL to the currently showing payment
request flow in Java without verifying its origin. This could allow a
compromised renderer to hijack the bottom sheet UI of a concurrent,
legitimate payment flow if the C++ origin checks are bypassed (e.g., via
a stalled service worker event for the compromised renderer).

This CL adds an origin verification check in the Java layer before
opening a payment handler window, ensuring that the requested GURL's
origin matches the scope/origin of the invoked payment app.

Bug: b:500472958
Test: components_junit_tests --gtest_filter="*PaymentRequestServiceTest*"
Change-Id: I3c86d2cc25eed46a78d31af7f9487ba77adbd0f3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7904676
Commit-Queue: Stephen McGruer <smcgruer@chromium.org>
Reviewed-by: Darwin Yang <darwinyang@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1643149}
---

diff --git a/components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.java b/components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.java
index a190112..11fde37 100644
--- a/components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.java
+++ b/components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.java
@@ -674,8 +674,20 @@
         }
 
         PaymentApp invokedPaymentApp = showingPaymentRequest.mInvokedPaymentApp;
-        assert invokedPaymentApp != null;
-        assert invokedPaymentApp.getPaymentAppType() == PaymentAppType.SERVICE_WORKER_APP;
+        if (invokedPaymentApp == null
+                || invokedPaymentApp.getPaymentAppType() != PaymentAppType.SERVICE_WORKER_APP) {
+            return null;
+        }
+
+        // Ensure that this request is for the same origin as the invoked payment app,
+        // to prevent a compromised renderer for one payment app from hijacking the UI of
+        // a different payment app.
+        Origin appOrigin = Origin.create(new GURL(invokedPaymentApp.getIdentifier()));
+        Origin windowOrigin = Origin.create(url);
+        if (!appOrigin.equals(windowOrigin)) {
+            return null;
+        }
+
         assumeNonNull(showingPaymentRequest.mBrowserPaymentRequest);
         return showingPaymentRequest.mBrowserPaymentRequest.openPaymentHandlerWindow(
                 url, invokedPaymentApp.getUkmSourceId());
diff --git a/components/payments/content/android/junit/src/org/chromium/components/payments/PaymentRequestServiceTest.java b/components/payments/content/android/junit/src/org/chromium/components/payments/PaymentRequestServiceTest.java
index d8997a4..f925de8 100644
--- a/components/payments/content/android/junit/src/org/chromium/components/payments/PaymentRequestServiceTest.java
+++ b/components/payments/content/android/junit/src/org/chromium/components/payments/PaymentRequestServiceTest.java
@@ -40,6 +40,7 @@
 import org.chromium.payments.mojom.PaymentOptions;
 import org.chromium.payments.mojom.PaymentRequestClient;
 import org.chromium.payments.mojom.PaymentResponse;
+import org.chromium.url.GURL;
 import org.chromium.url.mojom.Url;
 
 import java.util.ArrayList;
@@ -958,4 +959,81 @@
                 "Insecure");
         assertErrorAndReason("Insecure", PaymentErrorReason.NOT_ALLOWED_ERROR);
     }
+
+    @Test
+    @Feature({"Payments"})
+    public void testOpenPaymentHandlerWindow_noPaymentFlow() {
+        GURL targetUrl = new GURL("https://alice.example/pay");
+        Assert.assertNull(PaymentRequestService.openPaymentHandlerWindow(targetUrl));
+    }
+
+    @Test
+    @Feature({"Payments"})
+    public void testOpenPaymentHandlerWindow_noAppInvoked() {
+        GURL targetUrl = new GURL("https://alice.example/pay");
+        PaymentRequestService service = defaultBuilder().build();
+        show(service);
+        Assert.assertNull(PaymentRequestService.openPaymentHandlerWindow(targetUrl));
+    }
+
+    @Test
+    @Feature({"Payments"})
+    public void testOpenPaymentHandlerWindow_nativeAppInvoked() {
+        GURL targetUrl = new GURL("https://alice.example/pay");
+        PaymentRequestService service = defaultBuilder().build();
+        show(service);
+
+        AndroidPaymentApp nativeApp = Mockito.mock(AndroidPaymentApp.class);
+        Mockito.doReturn(PaymentAppType.NATIVE_MOBILE_APP).when(nativeApp).getPaymentAppType();
+        Mockito.doReturn("alice.example.app").when(nativeApp).packageName();
+        service.invokePaymentApp(nativeApp, Mockito.mock(PaymentResponseHelperInterface.class));
+
+        // A request to open the payment handler window should be denied because the invoked app
+        // is not a service worker app.
+        Assert.assertNull(PaymentRequestService.openPaymentHandlerWindow(targetUrl));
+    }
+
+    @Test
+    @Feature({"Payments"})
+    public void testOpenPaymentHandlerWindow_sameOrigin() {
+        GURL targetUrl = new GURL("https://alice.example/pay");
+        PaymentRequestService service = defaultBuilder().build();
+        show(service);
+
+        PaymentApp swAppSameOrigin = Mockito.mock(PaymentApp.class);
+        Mockito.doReturn(PaymentAppType.SERVICE_WORKER_APP)
+                .when(swAppSameOrigin)
+                .getPaymentAppType();
+        Mockito.doReturn("https://alice.example/scope").when(swAppSameOrigin).getIdentifier();
+        service.invokePaymentApp(
+                swAppSameOrigin, Mockito.mock(PaymentResponseHelperInterface.class));
+
+        WebContents mockWebContents = Mockito.mock(WebContents.class);
+        Mockito.doReturn(mockWebContents)
+                .when(mBrowserPaymentRequest)
+                .openPaymentHandlerWindow(Mockito.any(), Mockito.anyLong());
+
+        Assert.assertEquals(
+                mockWebContents, PaymentRequestService.openPaymentHandlerWindow(targetUrl));
+    }
+
+    @Test
+    @Feature({"Payments"})
+    public void testOpenPaymentHandlerWindow_crossOrigin() {
+        GURL targetUrl = new GURL("https://alice.example/pay");
+        PaymentRequestService service = defaultBuilder().build();
+        show(service);
+
+        PaymentApp swAppCrossOrigin = Mockito.mock(PaymentApp.class);
+        Mockito.doReturn(PaymentAppType.SERVICE_WORKER_APP)
+                .when(swAppCrossOrigin)
+                .getPaymentAppType();
+        Mockito.doReturn("https://bob.example/scope").when(swAppCrossOrigin).getIdentifier();
+        service.invokePaymentApp(
+                swAppCrossOrigin, Mockito.mock(PaymentResponseHelperInterface.class));
+
+        // A request to open the payment handler window should be denied because the invoked app
+        // has a different origin scope than the target URL.
+        Assert.assertNull(PaymentRequestService.openPaymentHandlerWindow(targetUrl));
+    }
 }
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Android Payment Handler BottomSheet hijack via confused-deputy

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 compromised renderer hosting any service worker can potentially hijack an active Payment Handler BottomSheet on Android to display an attacker-controlled page. This occurs because the fallback mechanism for opening payment handler windows on Android loses the caller’s identity and blindly attaches the requested URL to a process-wide static singleton representing the active payment flow.

Affected files:

  • components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.java
  • content/browser/service_worker/service_worker_version.cc
  • chrome/android/java/src/org/chromium/chrome/browser/ServiceTabLauncher.java
  • chrome/android/java/src/org/chromium/chrome/browser/payments/handler/PaymentHandlerCoordinator.java

Estimated timestamp from git blame: 2025-04-22

Description

On Android, a security vulnerability exists where a compromised renderer can inject an attacker-controlled page into the Payment Handler BottomSheet for an unrelated, in-progress payment flow. This is a confused-deputy issue resulting from a lack of caller authorization checks and the loss of context during a C++ to Java JNI transition.

Root Cause

The vulnerability stems from a sequence of gaps in validation and routing:

  1. Missing Authorization Check in C++: When a renderer sends the blink::mojom::ServiceWorkerHost::OpenPaymentHandlerWindow Mojo message, ServiceWorkerVersion::OpenPaymentHandlerWindow (service_worker_version.cc) only validates that the requested URL is same-origin with the calling Service Worker. Crucially, it does not verify if the calling Service Worker is actually authorized to interact with the current payment flow (e.g., by checking if it has a pending PaymentRequestEvent).

  2. Android Fallback and Identity Loss: PaymentHandlerSupport::ShowPaymentHandlerWindow attempts to call ChromeContentBrowserClient::ShowPaymentHandlerWindow. On Android, this method explicitly returns false. This triggers a fallback mechanism in ShowPaymentHandlerWindowReplier::~ShowPaymentHandlerWindowReplier, which routes the request to ServiceWorkerVersion::OpenWindow with a NEW_POPUP disposition. This request is eventually routed to JNI via ServiceTabLauncher::LaunchTab. During this transition, the identity of the calling Service Worker is lost.

  3. Blind Dispatch in Java: In Java, ServiceTabLauncher.launchTab receives the NEW_POPUP request and blindly calls the static method PaymentRequestService.openPaymentHandlerWindow(url).

  4. Process-Global Singleton Hijack: PaymentRequestService.openPaymentHandlerWindow(url) retrieves the currently active payment flow from a process-wide static singleton (BrowserGlobalPaymentFlowManager.sShowingPaymentRequest). It then attaches the attacker-provided url to this active flow without verifying that the URL’s origin matches the origin of the legitimately invoked payment app (mInvokedPaymentApp).

Potential Attack Scenario

Note: These are suggested steps; our tooling agent does not yet have the ability to run code.

  1. An attacker compromises a renderer process (e.g., via a v8 exploit) and controls a Service Worker at attacker.example.
  2. A victim initiates a legitimate PaymentRequest on merchant.example and selects a legitimate payment app (e.g., bank.example).
  3. The browser begins the invocation process, setting mInvokedPaymentApp to bank.example and storing the active flow in BrowserGlobalPaymentFlowManager.sShowingPaymentRequest.
  4. The compromised renderer sends the OpenPaymentHandlerWindow Mojo IPC with the URL https://attacker.example/phish.
  5. The browser process validates the URL is same-origin with the attacker’s Service Worker and routes the request through the Android fallback mechanism.
  6. ServiceTabLauncher calls PaymentRequestService.openPaymentHandlerWindow, which attaches the attacker’s phishing URL to the victim’s active payment flow.
  7. The Payment Handler BottomSheet opens, displaying the attacker’s phishing page instead of the intended bank.example interface.

Because sShowingPaymentRequest is process-wide, this could potentially allow an attacker in a regular profile to hijack a payment flow in an Incognito profile if they occur concurrently.

(Note: Exploitation requires a timing constraint to ensure the Mojo IPC arrives after mInvokedPaymentApp is set, otherwise a Java NullPointerException occurs due to an assert in PaymentRequestService.java:648.)

Suggested Fix

  1. C++ Validation: In ServiceWorkerVersion::OpenPaymentHandlerWindow (or at the PaymentRequestEvent dispatch level), verify that the calling Service Worker actually has a pending PaymentRequestEvent and is the currently authorized handler for an active payment request before proceeding.
  2. Java Validation: In PaymentRequestService.openPaymentHandlerWindow, add a strict check to ensure that the origin of the provided url matches the origin (or scope) of mInvokedPaymentApp before opening the window.

Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234


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