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
Tracker514006959
Fix commit78ac67149bfb (chromium/src) +112/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • components/payments/content/payment_app.cc
  • components/payments/content/payment_app.h
  • components/payments/content/payment_request.cc
  • components/payments/content/payment_request_display_manager.cc
  • components/payments/content/payment_request_display_manager.h
  • components/payments/content/payment_request_display_manager_unittest.cc
From 78ac67149bfb9d48f719c9ac295b1abd9b52923f Mon Sep 17 00:00:00 2001
From: Luis Antunes <[email protected]>
Date: Mon, 29 Jun 2026 12:13:01 -0700
Subject: [PATCH] [Payments] Restrict payment handler window to same-origin URLs

This CL mitigates a confused deputy UI spoofing vulnerability in payment
handlers.

Previously, when a user selected a payment app, the browser did not
verify if the Service Worker requesting to open the payment handler
window (via the OpenPaymentHandlerWindow Mojo IPC) actually matched the
selected app. This allowed a malicious Service Worker to hijack the
active payment dialog and display a cross-origin phishing URL.

This CL fixes this by storing the origin of the selected payment app and
verifying that the URL requested by the Service Worker is same-origin
with it.

Changes:
- Added GetPaymentHandlerOrigin() to retrieve the active app's origin.
- Store this origin in the display manager when the user starts paying.
- Check that the target window URL is same-origin with the stored origin.
- Added tests to verify same-origin, mismatched, and uninitialized states.

Fixed: 514006959
Change-Id: Iefcf127b16d542f727719f0375438e2db340f13e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7979328
Reviewed-by: Slobodan Pejic <[email protected]>
Reviewed-by: Darwin Yang <[email protected]>
Commit-Queue: Luis Antunes <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1654256}
---

diff --git a/components/payments/content/payment_app.cc b/components/payments/content/payment_app.cc
index e1d0d30c8..3ee325a 100644
--- a/components/payments/content/payment_app.cc
+++ b/components/payments/content/payment_app.cc
@@ -5,8 +5,10 @@
 #include "components/payments/content/payment_app.h"
 
 #include <algorithm>
+#include <optional>
 
 #include "base/functional/callback.h"
+#include "url/origin.h"
 
 namespace payments {
 namespace {
@@ -158,4 +160,8 @@
   return false;
 }
 
+std::optional<url::Origin> PaymentApp::GetPaymentHandlerOrigin() const {
+  return std::nullopt;
+}
+
 }  // namespace payments
diff --git a/components/payments/content/payment_app.h b/components/payments/content/payment_app.h
index e1d76b19..c33eef1 100644
--- a/components/payments/content/payment_app.h
+++ b/components/payments/content/payment_app.h
@@ -5,6 +5,7 @@
 #ifndef COMPONENTS_PAYMENTS_CONTENT_PAYMENT_APP_H_
 #define COMPONENTS_PAYMENTS_CONTENT_PAYMENT_APP_H_
 
+#include <optional>
 #include <set>
 #include <string>
 #include <vector>
@@ -17,6 +18,7 @@
 #include "third_party/blink/public/mojom/payments/payment_app_events.mojom.h"
 #include "third_party/blink/public/mojom/payments/payment_handler_host.mojom.h"
 #include "third_party/skia/include/core/SkBitmap.h"
+#include "url/origin.h"
 
 namespace payments {
 
@@ -107,6 +109,9 @@
   // GUID of an autofill card or the scope of a payment handler.
   virtual std::string GetId() const = 0;
 
+  // Returns the origin of the payment handler, if applicable.
+  virtual std::optional<url::Origin> GetPaymentHandlerOrigin() const;
+
   // Return the sub/label of payment app, to be displayed to the user.
   virtual std::u16string GetLabel() const = 0;
   virtual std::u16string GetSublabel() const = 0;
diff --git a/components/payments/content/payment_request.cc b/components/payments/content/payment_request.cc
index e23b15d..4a9d3a16 100644
--- a/components/payments/content/payment_request.cc
+++ b/components/payments/content/payment_request.cc
@@ -5,6 +5,7 @@
 #include "components/payments/content/payment_request.h"
 
 #include <algorithm>
+#include <optional>
 #include <string>
 #include <utility>
 
@@ -48,6 +49,7 @@
 #include "services/metrics/public/cpp/ukm_source_id.h"
 #include "services/network/public/cpp/is_potentially_trustworthy.h"
 #include "third_party/blink/public/common/features.h"
+#include "url/origin.h"
 
 namespace payments {
 namespace {
@@ -1150,6 +1152,9 @@
   // Log the correct "selected method".
   journey_logger_.SetSelectedMethod(GetSelectedMethodCategory());
 
+  display_handle_->SetPaymentHandlerOrigin(
+      state_->selected_app()->GetPaymentHandlerOrigin());
+
   state_->selected_app()->SetPaymentHandlerHost(
       payment_handler_host_->AsWeakPtr());
   state_->GeneratePaymentResponse();
diff --git a/components/payments/content/payment_request_display_manager.cc b/components/payments/content/payment_request_display_manager.cc
index 84f709f..6c831830 100644
--- a/components/payments/content/payment_request_display_manager.cc
+++ b/components/payments/content/payment_request_display_manager.cc
@@ -4,9 +4,12 @@
 
 #include "components/payments/content/payment_request_display_manager.h"
 
+#include <optional>
+
 #include "base/check.h"
 #include "base/metrics/histogram_functions.h"
 #include "components/payments/content/content_payment_request_delegate.h"
+#include "url/origin.h"
 
 namespace payments {
 
@@ -82,11 +85,22 @@
     delegate_->RetryDialog();
 }
 
+void PaymentRequestDisplayManager::DisplayHandle::SetPaymentHandlerOrigin(
+    std::optional<url::Origin> origin) {
+  payment_handler_origin_ = std::move(origin);
+}
+
 void PaymentRequestDisplayManager::DisplayHandle::DisplayPaymentHandlerWindow(
     const GURL& url,
     PaymentHandlerOpenWindowCallback callback) {
-  if (delegate_)
-    delegate_->EmbedPaymentHandlerWindow(url, std::move(callback));
+  if (!delegate_ || !payment_handler_origin_ ||
+      !payment_handler_origin_->IsSameOriginWith(url::Origin::Create(url))) {
+    std::move(callback).Run(/*success=*/false, /*render_process_id=*/0,
+                            /*render_frame_id=*/0);
+    return;
+  }
+
+  delegate_->EmbedPaymentHandlerWindow(url, std::move(callback));
 }
 
 base::WeakPtr<PaymentRequestDisplayManager::DisplayHandle>
diff --git a/components/payments/content/payment_request_display_manager.h b/components/payments/content/payment_request_display_manager.h
index f844ec0..3eea4ce 100644
--- a/components/payments/content/payment_request_display_manager.h
+++ b/components/payments/content/payment_request_display_manager.h
@@ -6,12 +6,14 @@
 #define COMPONENTS_PAYMENTS_CONTENT_PAYMENT_REQUEST_DISPLAY_MANAGER_H_
 
 #include <memory>
+#include <optional>
 
 #include "base/functional/callback.h"
 #include "base/memory/weak_ptr.h"
 #include "components/keyed_service/core/keyed_service.h"
 #include "content/public/browser/render_frame_host.h"
 #include "url/gurl.h"
+#include "url/origin.h"
 
 namespace payments {
 
@@ -64,6 +66,9 @@
     void DisplayPaymentHandlerWindow(const GURL& url,
                                      PaymentHandlerOpenWindowCallback callback);
 
+    // Set the expected origin of the payment handler.
+    void SetPaymentHandlerOrigin(std::optional<url::Origin> origin);
+
     // Returns true after Show() was called.
     bool was_shown() const { return was_shown_; }
 
@@ -77,6 +82,7 @@
     base::WeakPtr<PaymentRequestDisplayManager> display_manager_;
     base::WeakPtr<ContentPaymentRequestDelegate> delegate_;
     bool was_shown_ = false;
+    std::optional<url::Origin> payment_handler_origin_;
 
     base::WeakPtrFactory<DisplayHandle> weak_ptr_factory_{this};
   };
diff --git a/components/payments/content/payment_request_display_manager_unittest.cc b/components/payments/content/payment_request_display_manager_unittest.cc
index 14b84240..f225e8327 100644
--- a/components/payments/content/payment_request_display_manager_unittest.cc
+++ b/components/payments/content/payment_request_display_manager_unittest.cc
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/payments/content/payment_request_display_manager_unittest.cc b/components/payments/content/payment_request_display_manager_unittest.cc
index 14b84240..f225e8327 100644
--- a/components/payments/content/payment_request_display_manager_unittest.cc
+++ b/components/payments/content/payment_request_display_manager_unittest.cc
@@ -6,12 +6,15 @@
 
 #include "base/memory/weak_ptr.h"
 #include "base/test/metrics/histogram_tester.h"
+#include "base/test/mock_callback.h"
 #include "components/payments/content/mock_content_payment_request_delegate.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/test_renderer_host.h"
 #include "content/public/test/web_contents_tester.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+#include "url/origin.h"
 
 namespace payments {
 
@@ -117,4 +120,64 @@
       /*expected_bucket_count=*/1);
 }
 
+TEST_F(PaymentRequestDisplayManagerTest,
+       ShowPaymentHandlerWindow_NoInvokedApp) {
+  PaymentRequestDisplayManager display_manager;
+  MockContentPaymentRequestDelegate mock_delegate;
+  std::unique_ptr<PaymentRequestDisplayManager::DisplayHandle> handle =
+      display_manager.TryShow(mock_delegate.GetContentWeakPtr());
+  ASSERT_TRUE(handle);
+
+  base::MockCallback<PaymentHandlerOpenWindowCallback> callback;
+  EXPECT_CALL(callback, Run(/*success=*/false, /*render_process_id=*/0,
+                            /*render_frame_id=*/0));
+
+  handle->DisplayPaymentHandlerWindow(GURL("https://example.com"),
+                                      callback.Get());
+}
+
+TEST_F(PaymentRequestDisplayManagerTest,
+       ShowPaymentHandlerWindow_MatchingOrigin) {
+  PaymentRequestDisplayManager display_manager;
+  MockContentPaymentRequestDelegate mock_delegate;
+  std::unique_ptr<PaymentRequestDisplayManager::DisplayHandle> handle =
+      display_manager.TryShow(mock_delegate.GetContentWeakPtr());
+  ASSERT_TRUE(handle);
+
+  url::Origin origin = url::Origin::Create(GURL("https://example.com"));
+  handle->SetPaymentHandlerOrigin(origin);
+
+  base::MockCallback<PaymentHandlerOpenWindowCallback> callback;
+  EXPECT_CALL(mock_delegate, EmbedPaymentHandlerWindow(
+                                 GURL("https://example.com/pay"), testing::_))
+      .WillOnce([](const GURL& url, PaymentHandlerOpenWindowCallback cb) {
+        std::move(cb).Run(/*success=*/true, /*render_process_id=*/1,
+                          /*render_frame_id=*/2);
+      });
+  EXPECT_CALL(callback, Run(/*success=*/true, /*render_process_id=*/1,
+                            /*render_frame_id=*/2));
+
+  handle->DisplayPaymentHandlerWindow(GURL("https://example.com/pay"),
+                                      callback.Get());
+}
+
+TEST_F(PaymentRequestDisplayManagerTest,
+       ShowPaymentHandlerWindow_MismatchingOrigin) {
+  PaymentRequestDisplayManager display_manager;
+  MockContentPaymentRequestDelegate mock_delegate;
+  std::unique_ptr<PaymentRequestDisplayManager::DisplayHandle> handle =
+      display_manager.TryShow(mock_delegate.GetContentWeakPtr());
+  ASSERT_TRUE(handle);
+
+  url::Origin origin = url::Origin::Create(GURL("https://example.com"));
+  handle->SetPaymentHandlerOrigin(origin);
+
+  base::MockCallback<PaymentHandlerOpenWindowCallback> callback;
+  EXPECT_CALL(callback, Run(/*success=*/false, /*render_process_id=*/0,
+                            /*render_frame_id=*/0));
+
+  handle->DisplayPaymentHandlerWindow(GURL("https://not-example.com/pay"),
+                                      callback.Get());
+}
+
 }  // namespace payments
Loading diff…

Original Bug Report

reported by [email protected]

Confused Deputy in PaymentRequestDisplayManager allows hijacked Payment Handler UI

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A compromised renderer process can exploit a lack of caller validation to hijack an active Payment Request dialog in a different tab within the same profile. By invoking the OpenPaymentHandlerWindow Mojo IPC, an attacker can inject an arbitrary same-origin URL into a trusted browser-modal dialog belonging to another origin’s payment flow. If no payment app is currently selected in the target dialog, this action also triggers a null pointer dereference, leading to a browser process crash.

Affected files:

  • components/payments/content/payment_request_display_manager.cc
  • content/browser/service_worker/service_worker_version.cc
  • chrome/browser/payments/chrome_payment_request_delegate.cc
  • chrome/browser/ui/views/payments/payment_request_dialog_view.cc
  • chrome/browser/ui/views/payments/payment_handler_web_flow_view_controller.cc

Estimated timestamp from git blame: 2018-01-12

Vulnerability Description

A logic flaw in the Payment Request implementation on desktop platforms creates a ‘confused deputy’ scenario. The PaymentRequestDisplayManager, a profile-scoped singleton responsible for coordinating payment UIs, routes requests to open Payment Handler windows to the currently active payment flow without verifying the identity of the caller.

The vulnerability consists of two primary failures in the browser process:

  1. Missing Authorization in ServiceWorkerVersion: The Mojo IPC handler ServiceWorkerVersion::OpenPaymentHandlerWindow (exposed via blink::mojom::ServiceWorkerHost) fails to verify that the calling service worker is authorized to open a window. Specifically, it does not check if the service worker is currently handling an active PaymentRequestEvent initiated by a legitimate merchant flow.
  2. Lack of Origin Validation in PaymentRequestDisplayManager: The request is passed to PaymentRequestDisplayManager::ShowPaymentHandlerWindow, which immediately dispatches the URL to the current_handle_ (the active payment flow for the profile) if one exists. It performs no check to ensure the service worker’s origin matches the payment app intended for that handle.

Potential Attack Scenario (Unverified)

  1. Preparation: An attacker achieves code execution in a renderer process hosting a service worker for https://attacker.com (e.g., via a separate vulnerability or by hosting a malicious site).
  2. Victim Initiation: A user initiates a legitimate payment on https://merchant.com in a separate tab. The browser displays a modal PaymentRequest dialog. This dialog is now stored as the current_handle_ in the profile’s PaymentRequestDisplayManager.
  3. Exploitation: The compromised renderer invokes OpenPaymentHandlerWindow for the attacker’s service worker, providing the URL https://attacker.com/phish.
  4. Hijacking: The browser process incorrectly routes this request to the merchant’s dialog. The dialog transitions to the Payment Handler screen, loading the attacker’s content.
  5. Impact: The attacker controls the content of a trusted browser-modal dialog. If a legitimate payment app (e.g., Google Pay) was already selected, the dialog header may even display the legitimate app’s icon (retrieved from the merchant’s PaymentRequestState), while the content area hosts the phishing page.

Potential DoS (Null Pointer Dereference)

If the OpenPaymentHandlerWindow IPC is sent while a PaymentRequest dialog is open but before the user has selected a specific payment app, PaymentHandlerWebFlowViewController::PopulateSheetHeaderView will attempt to dereference state()->selected_app() (which is nullptr), resulting in a deterministic browser process crash.

Affected Locations

  • content/browser/service_worker/service_worker_version.cc: OpenPaymentHandlerWindow lacks authorization checks.
  • components/payments/content/payment_request_display_manager.cc: ShowPaymentHandlerWindow lacks caller origin validation.
  • chrome/browser/ui/views/payments/payment_handler_web_flow_view_controller.cc: PopulateSheetHeaderView lacks a null check for selected_app().

Suggested Fix

  1. In ServiceWorkerVersion, implement a check to ensure that OpenPaymentHandlerWindow is only permitted if the version is currently processing a valid PaymentRequestEvent (e.g., by checking the count of inflight payment-related requests).
  2. In PaymentRequestDisplayManager::ShowPaymentHandlerWindow, validate that the origin of the calling service worker matches the origin of the payment app associated with the current_handle_.
  3. Add a null pointer check in PaymentHandlerWebFlowViewController::PopulateSheetHeaderView for state()->selected_app() to prevent the browser crash.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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