CVE-2026-17791
Overview
Files Changed
components/payments/content/payment_app.cccomponents/payments/content/payment_app.hcomponents/payments/content/payment_request.cccomponents/payments/content/payment_request_display_manager.cccomponents/payments/content/payment_request_display_manager.hcomponents/payments/content/payment_request_display_manager_unittest.cc
Patch
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
Regression Test / PoC
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
Original Bug Report
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.cccontent/browser/service_worker/service_worker_version.ccchrome/browser/payments/chrome_payment_request_delegate.ccchrome/browser/ui/views/payments/payment_request_dialog_view.ccchrome/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:
- Missing Authorization in
ServiceWorkerVersion: The Mojo IPC handlerServiceWorkerVersion::OpenPaymentHandlerWindow(exposed viablink::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 activePaymentRequestEventinitiated by a legitimate merchant flow. - Lack of Origin Validation in
PaymentRequestDisplayManager: The request is passed toPaymentRequestDisplayManager::ShowPaymentHandlerWindow, which immediately dispatches the URL to thecurrent_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)
- 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). - Victim Initiation: A user initiates a legitimate payment on
https://merchant.comin a separate tab. The browser displays a modalPaymentRequestdialog. This dialog is now stored as thecurrent_handle_in the profile’sPaymentRequestDisplayManager. - Exploitation: The compromised renderer invokes
OpenPaymentHandlerWindowfor the attacker’s service worker, providing the URLhttps://attacker.com/phish. - 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.
- 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:OpenPaymentHandlerWindowlacks authorization checks.components/payments/content/payment_request_display_manager.cc:ShowPaymentHandlerWindowlacks caller origin validation.chrome/browser/ui/views/payments/payment_handler_web_flow_view_controller.cc:PopulateSheetHeaderViewlacks a null check forselected_app().
Suggested Fix
- In
ServiceWorkerVersion, implement a check to ensure thatOpenPaymentHandlerWindowis only permitted if the version is currently processing a validPaymentRequestEvent(e.g., by checking the count of inflight payment-related requests). - In
PaymentRequestDisplayManager::ShowPaymentHandlerWindow, validate that the origin of the calling service worker matches the origin of the payment app associated with thecurrent_handle_. - Add a null pointer check in
PaymentHandlerWebFlowViewController::PopulateSheetHeaderViewforstate()->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.