CVE-2026-11019
Overview
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactory.javachrome/browser/payments/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactoryTest.java
Patch
From b5b9a3cf7cf81e959730d08e6443fd8d1ae98792 Mon Sep 17 00:00:00 2001 From: Luis Antunes <[email protected]> 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 <[email protected]> Reviewed-by: Darwin Yang <[email protected]> Commit-Queue: Luis Antunes <[email protected]> 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(
Original Bug Report
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.javacomponents/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.javachrome/android/java/src/org/chromium/chrome/browser/payments/ui/PaymentUiService.javacomponents/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.
- An attacker sets up
https://attacker.exampleand compromises the renderer process hosting it. - The compromised renderer navigates the tab to
https://victim.example(e.g., a banking site). https://victim.examplecommits and becomes the activeRenderFrameHost.https://attacker.exampleis moved into the BFCache (LifecycleState.kInBackForwardCache).- Ignoring the BFCache freeze signal, the compromised renderer sends a Mojo
GetInterfacerequest forpayments.mojom.PaymentRequestvia itsBrowserInterfaceBroker. ChromePaymentRequestFactory.createImpl()binds the interface because it omits theLifecycleStatecheck.- The attacker sends a
PaymentRequest.InitIPC.PaymentRequestService.init()reads the tab’s currentWebContents, fetching the victim’s URL and title formTopLevelOriginandmMerchantName. - The attacker sends a
PaymentRequest.Show(..., had_user_activation=true)IPC. - The Android payment flow initiates, passing the spoofed
EXTRA_TOP_ORIGINto external Android payment apps, deceiving the user into authorizing a payment seemingly requested byvictim.example.
Suggested Fix
- Update
ChromePaymentRequestFactory.createImpl()inchrome/android/java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactory.javato check theRenderFrameHostlifecycle state before allowing the binding, similar toAwPaymentRequestFactory:
if (mRenderFrameHost == null
|| !mRenderFrameHost.isRenderFrameLive()
|| mRenderFrameHost.getLifecycleState() != LifecycleState.ACTIVE) {
return new InvalidPaymentRequest();
}
- As a defense-in-depth measure,
PaymentRequestServiceshould fetch the origin and title from theRenderFrameHostthat initiated the request, rather than relying on the tab-levelWebContents.
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.