Medium CVSS 4.3 chrome Policy Bypass 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Payments
DescriptionInsufficient policy enforcement in Payments
ComponentPayments
Sub-componentBug
Bug ClassPolicy Bypass
Tracker470646792
Fix commit4d76210c7828 (chromium/src) +82/-7
CWECWE-284
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedJorian Woltjer
Reported2025-12-21 (per vendor advisory)
Fix landed2026-07-14 (public git)
Disclosed2026-05-12
Patch gapNot computed: several commits cite this bug and it is not yet established which one is the fix.

Background

Payment Handler modal dialog (Clank/Android)
When a merchant site invokes the Payment Request API with a web-based payment handler, Chrome for Android opens the payment app’s URL in a bottom-sheet WebContents managed by PaymentHandlerCoordinator.
Initiator origin
The origin credited with starting a navigation. The browser uses it (via LoadUrlParams.setInitiatorOrigin) to decide request context, including whether cross-site requests should drop SameSite cookies.
`SameSite` cookies
Cookies restricted to same-site contexts; whether they are sent depends on the navigation’s initiator/site relationship. A navigation with no (or a “browser”/opaque) initiator can be treated as top-level, causing SameSite=Lax/Strict cookies to be attached as if first-party.
`PaymentFeatureList.PAYMENT_HANDLER_DIALOG_USE_INITIATOR_IN_URL_LOAD`
Feature flag gating the new behavior of attaching the merchant origin as the initiator.

Root Cause Analysis

Before the patch, PaymentHandlerCoordinator.initializeWebContents loaded the payment app URL with getNavigationController().loadUrl(new LoadUrlParams(url.getSpec())) and set no initiator origin. On Android this meant the payment-handler dialog’s navigation to the payment app was performed without crediting the merchant page as initiator, so the browser treated it like a browser-initiated/top-level load. As a result SameSite cookies for the payment app’s site were attached as though the request were first-party, even though it was effectively driven by the merchant site — a SameSite cookie bypass (the commit message is explicit: “This fixes the SameSite cookie bypass security vulnerability,” and notes the desktop equivalent was already fixed in crrev.com/e158cad6).

The patch threads the merchant’s WebContents (paymentRequestWebContents) down into initializeWebContents and, when the PAYMENT_HANDLER_DIALOG_USE_INITIATOR_IN_URL_LOAD feature is enabled, builds LoadUrlParams and calls params.setInitiatorOrigin(Origin.create(paymentRequestWebContents.getLastCommittedUrl())) before loadUrl(params). By attaching the merchant origin as initiator, the navigation is correctly classified relative to the payment app’s site, so SameSite cookie rules apply as intended and the first-party treatment is no longer wrongly granted. The remaining changes are plumbing (show now passes the merchant WebContents, a test-only WebContentsObserver hook) and an instrumentation test (testInitiatorOriginSet) asserting the navigation’s initiator origin equals the merchant origin.

Key insight
The Android payment-handler dialog loaded the payment app URL with no initiator origin, so cross-site navigation was treated as first-party and SameSite cookies leaked; the fix sets the merchant page as the navigation initiator.

Attack Path

  1. Victim visits an attacker-influenced merchant page which invokes the Payment Request API pointing at a payment handler on a target site. (inferred from component)
  2. Chrome opens the payment-handler dialog PaymentHandlerCoordinator.show creates a WebContents and loads the payment app URL with no initiator. (shown by the pre-patch loadUrl(new LoadUrlParams(url.getSpec())))
  3. Navigation treated as top-level/first-party because the initiator origin is absent, the browser does not down-rank the request to cross-site. (root cause)
  4. `SameSite` cookies attached SameSite=Lax/Strict cookies for the payment app’s site are sent as if first-party, exposing authenticated state to a merchant-driven context. (from commit message)
  5. Primitive Cross-site SameSite cookie bypass: the merchant context obtains/uses the victim’s first-party cookie context on the payment site. No memory corruption.

Impact Assessment

Browser-process navigation/cookie policy on Android (Clank); web-reachable via the Payment Request API with user interaction to open the dialog (UI:R). The attacker gains a SameSite cookie bypass — authenticated cross-site state attached to a merchant-initiated payment-handler navigation (CVSS 4.3, integrity-only) — not RCE. The fix is gated behind PAYMENT_HANDLER_DIALOG_USE_INITIATOR_IN_URL_LOAD, so protection depends on that feature being enabled; the desktop path was fixed separately. No freed memory or escalation is involved.

Changed Functions

FunctionChangeNotes
if
chrome/android/java/src/org/chromium/chrome/browser/payments/handler/PaymentHandlerCoordinator.java
modified

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/payments/handler/PaymentHandlerCoordinator.java
  • chrome/android/javatests/src/org/chromium/chrome/browser/payments/ExpandablePaymentHandlerTest.java

Audit Directions

  • Other browser-created WebContents loads
    Audit places that call loadUrl(new LoadUrlParams(...)) for browser-managed dialogs/sheets (payments, WebAuthn, share, print preview) without setInitiatorOrigin, which can mis-classify navigations for SameSite/CORS.
  • Feature-flag coverage
    Verify the fix is enabled by default across channels and that the desktop and Android paths are consistent; a disabled flag leaves the bypass.
  • Initiator-origin correctness
    Review uses of getLastCommittedUrl() to derive an initiator origin for timing/opaque-origin edge cases (about:blank, redirects) that could produce a wrong or opaque initiator.
From 4d76210c7828d2d67024ea1b3b0ba9ec7206d2a5 Mon Sep 17 00:00:00 2001
From: Jayden Nikifork <[email protected]>
Date: Tue, 14 Jul 2026 06:48:20 -0700
Subject: [PATCH] [bug][payment] Add initiator for clank payment handler dialog

Add the merchant site as the initiator for the payment handler modal
dialog popup. This fixes the SameSite cookie bypass security
vulnerability.

Note that this bug has already been fixed for desktop:
https://crrev.com/e158cad632facb1c5348ad9ca1a40216cdd1aa2a.

Bug: 470646792
Change-Id: I0321d25b40172aa851f1549d258c300485c9db88
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8071878
Reviewed-by: Darwin Yang <[email protected]>
Commit-Queue: Jayden Nikifork <[email protected]>
Reviewed-by: Xuehui Chen <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1661811}
---

diff --git a/chrome/android/java/src/org/chromium/chrome/browser/payments/handler/PaymentHandlerCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/payments/handler/PaymentHandlerCoordinator.java
index 7c025b0..4680580d 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/payments/handler/PaymentHandlerCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/payments/handler/PaymentHandlerCoordinator.java
@@ -23,6 +23,7 @@
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetControllerProvider;
 import org.chromium.components.embedder_support.view.ContentView;
+import org.chromium.components.payments.PaymentFeatureList;
 import org.chromium.components.payments.PaymentHandlerNavigationThrottle;
 import org.chromium.components.payments.ui.InputProtector;
 import org.chromium.components.thinwebview.ThinWebView;
@@ -33,6 +34,7 @@
 import org.chromium.content_public.browser.SelectionClient;
 import org.chromium.content_public.browser.SelectionPopupController;
 import org.chromium.content_public.browser.WebContents;
+import org.chromium.content_public.browser.WebContentsObserver;
 import org.chromium.ui.base.IntentRequestTracker;
 import org.chromium.ui.base.ViewAndroidDelegate;
 import org.chromium.ui.base.WindowAndroid;
@@ -40,6 +42,7 @@
 import org.chromium.ui.modelutil.PropertyModel;
 import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
 import org.chromium.url.GURL;
+import org.chromium.url.Origin;
 
 /**
  * PaymentHandler coordinator, which owns the component overall, i.e., creates other objects in the
@@ -52,6 +55,7 @@
     private @Nullable Runnable mHider;
     private @Nullable WebContents mPaymentHandlerWebContents;
     private @Nullable PaymentHandlerToolbarCoordinator mToolbarCoordinator;
+    private @Nullable WebContentsObserver mWebContentsObserverForTest;
     private InputProtector mInputProtector = new InputProtector();
 
     /** Constructs the payment-handler component coordinator. */
@@ -66,6 +70,10 @@
         void onPaymentHandlerUiShown();
     }
 
+    public void setWebContentsObserverForTest(WebContentsObserver observer) {
+        mWebContentsObserverForTest = observer;
+    }
+
     /**
      * Shows the payment-handler UI.
      *
@@ -90,10 +98,13 @@
         mInputProtector.markShowTime();
         mPaymentHandlerWebContents =
                 WebContentsFactory.createWebContents(profile, /* initiallyHidden= */ false, false);
+        if (mWebContentsObserverForTest != null) {
+            mWebContentsObserverForTest.observe(mPaymentHandlerWebContents);
+        }
         PaymentHandlerNavigationThrottle.markPaymentHandlerWebContents(mPaymentHandlerWebContents);
         ContentView webContentView =
                 ContentView.createContentView(activity, mPaymentHandlerWebContents);
-        initializeWebContents(windowAndroid, webContentView, url);
+        initializeWebContents(windowAndroid, webContentView, paymentRequestWebContents, url);
 
         mToolbarCoordinator =
                 new PaymentHandlerToolbarCoordinator(
@@ -186,7 +197,10 @@
 
     @RequiresNonNull("mPaymentHandlerWebContents")
     private void initializeWebContents(
-            WindowAndroid windowAndroid, ContentView webContentView, GURL url) {
+            WindowAndroid windowAndroid,
+            ContentView webContentView,
+            WebContents paymentRequestWebContents,
+            GURL url) {
         mPaymentHandlerWebContents.setDelegates(
                 VersionInfo.getProductVersion(),
                 ViewAndroidDelegate.createBasicDelegate(webContentView),
@@ -201,9 +215,13 @@
         controller.setSelectionClient(
                 SelectionClient.createSmartSelectionClient(mPaymentHandlerWebContents));
 
-        mPaymentHandlerWebContents
-                .getNavigationController()
-                .loadUrl(new LoadUrlParams(url.getSpec()));
+        LoadUrlParams params = new LoadUrlParams(url.getSpec());
+        if (PaymentFeatureList.isEnabled(
+                PaymentFeatureList.PAYMENT_HANDLER_DIALOG_USE_INITIATOR_IN_URL_LOAD)) {
+            params.setInitiatorOrigin(
+                    Origin.create(paymentRequestWebContents.getLastCommittedUrl()));
+        }
+        mPaymentHandlerWebContents.getNavigationController().loadUrl(params);
     }
 
     /**
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/payments/ExpandablePaymentHandlerTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/payments/ExpandablePaymentHandlerTest.java
index 52bf1fb..0c1a5717 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/payments/ExpandablePaymentHandlerTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/payments/ExpandablePaymentHandlerTest.java
@@ -40,6 +40,7 @@
 import org.chromium.base.test.util.DisableIf;
 import org.chromium.base.test.util.DisabledTest;
 import org.chromium.base.test.util.Feature;
+import org.chromium.base.test.util.Features.EnableFeatures;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.ChromeTabbedActivity;
 import org.chromium.chrome.browser.compositor.overlay_panel.OverlayPanel.StateChangeReason;
@@ -53,9 +54,11 @@
 import org.chromium.chrome.test.transit.page.WebPageStation;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetTestSupport;
 import org.chromium.components.embedder_support.util.UrlConstants;
+import org.chromium.components.payments.PaymentFeatureList;
 import org.chromium.components.payments.ui.InputProtector;
 import org.chromium.components.payments.ui.test_support.FakeClock;
 import org.chromium.content_public.browser.LoadUrlParams;
+import org.chromium.content_public.browser.NavigationHandle;
 import org.chromium.content_public.browser.WebContents;
 import org.chromium.content_public.browser.WebContentsObserver;
 import org.chromium.content_public.browser.test.util.DOMUtils;
@@ -64,6 +67,7 @@
 import org.chromium.net.test.ServerCertificate;
 import org.chromium.ui.base.DeviceFormFactor;
 import org.chromium.url.GURL;
+import org.chromium.url.Origin;
 
 import java.util.Arrays;
 import java.util.List;
@@ -144,16 +148,30 @@
         mClock = new FakeClock();
     }
 
-    private PaymentHandlerCoordinator createPaymentHandlerAndShow(ChromeTabbedActivity cta)
-            throws Throwable {
+    private PaymentHandlerCoordinator createPaymentHandler() throws Throwable {
         PaymentHandlerCoordinator paymentHandler = new PaymentHandlerCoordinator();
         paymentHandler.setInputProtectorForTest(new InputProtector(mClock));
+        return paymentHandler;
+    }
+
+    private void showPaymentHandler(
+            PaymentHandlerCoordinator paymentHandler, ChromeTabbedActivity cta) {
         ThreadUtils.runOnUiThreadBlocking(
                 () ->
                         paymentHandler.show(
                                 cta.getCurrentWebContents(),
                                 defaultPaymentAppUrl(),
                                 defaultUiObserver()));
+    }
+
+    private void showPaymentHandler(PaymentHandlerCoordinator paymentHandler) {
+        showPaymentHandler(paymentHandler, mDefaultActivity);
+    }
+
+    private PaymentHandlerCoordinator createPaymentHandlerAndShow(ChromeTabbedActivity cta)
+            throws Throwable {
+        PaymentHandlerCoordinator paymentHandler = createPaymentHandler();
+        showPaymentHandler(paymentHandler, cta);
         return paymentHandler;
     }
 
@@ -358,6 +376,42 @@
     @Test
     @SmallTest
     @Feature({"Payments"})
+    @EnableFeatures({PaymentFeatureList.PAYMENT_HANDLER_DIALOG_USE_INITIATOR_IN_URL_LOAD})
+    public void testInitiatorOriginSet() throws Throwable {
+        startDefaultServer();
+        mStartingPage = mStartingPage.loadWebPageProgrammatically(mServer.getURL("/"));
+
+        CallbackHelper startNavigationCallbackHelper = new CallbackHelper();
+        PaymentHandlerCoordinator paymentHandler = createPaymentHandler();
+
+        WebContentsObserver observer =
+                new WebContentsObserver() {
+                    @Override
+                    public void didStartNavigationInPrimaryMainFrame(NavigationHandle navigation) {
+                        Origin expectedOrigin =
+                                Origin.create(
+                                        mDefaultActivity
+                                                .getCurrentWebContents()
+                                                .getLastCommittedUrl());
+                        Assert.assertEquals(expectedOrigin, navigation.getInitiatorOrigin());
+                        startNavigationCallbackHelper.notifyCalled();
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.