CVE-2026-19175
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
SecurePaymentConfirmationSynchronousDestructionTestchrome/browser/payments/secure_payment_confirmation_browsertest.cc |
modified | |
ifcomponents/payments/content/payment_request.cc |
modified | |
ifcomponents/payments/content/payment_request_spec.cc |
modified | |
PaymentRequestSpecSynchronousDestructionTestcomponents/payments/content/payment_request_spec_unittest.cc |
modified | |
TEST_Fcomponents/payments/content/payment_request_spec_unittest.cc |
modified | |
ifcomponents/payments/content/payment_request_state.cc |
modified |
Files Changed
chrome/browser/payments/secure_payment_confirmation_browsertest.cccomponents/payments/content/payment_request.cccomponents/payments/content/payment_request_spec.cccomponents/payments/content/payment_request_spec_unittest.cccomponents/payments/content/payment_request_state.cccomponents/payments/content/payment_request_state_unittest.cc
Patch
From 9934ca9908d82b853b4391af74405398db9f1242 Mon Sep 17 00:00:00 2001 From: Stephen McGruer <[email protected]> Date: Thu, 30 Jul 2026 17:52:57 -0700 Subject: [PATCH] [payments] Avoid UAF for calls that can show an SPC modal When showing a Secure Payment Confirmation (SPC) web modal dialog via ShowWebModalDialogViews(), the modal delegate checks if the web contents is in fullscreen and if so synchronously calls ExitFullscreen(true). On platforms like Windows and macOS, this can spin a nested message loop or process pending window messages, destroying the WebContents and freeing PaymentRequest and its owned objects while they are still on the stack. This change adds base::WeakPtr self-liveness guards after synchronous re-entrant calls in: - PaymentRequestState::OnDoneCreatingPaymentApps() - PaymentRequestSpec::RecomputeSpecForDetails() - PaymentRequest::UpdateWith() - SecurePaymentConfirmationController:: SetupModelAndShowDialogIfApplicable Fixed: 540138836 Change-Id: I7bfc5e8685abe7bc708520fa48059f623d24eaec Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8162516 Commit-Queue: Stephen McGruer <[email protected]> Reviewed-by: Darwin Yang <[email protected]> Cr-Commit-Position: refs/heads/main@{#1671557} --- diff --git a/chrome/browser/payments/secure_payment_confirmation_browsertest.cc b/chrome/browser/payments/secure_payment_confirmation_browsertest.cc index 2e52323..c5ce281 100644 --- a/chrome/browser/payments/secure_payment_confirmation_browsertest.cc +++ b/chrome/browser/payments/secure_payment_confirmation_browsertest.cc @@ -523,6 +523,44 @@ test_controller()->CloseDialog(); } +class SecurePaymentConfirmationSynchronousDestructionTest + : public SecurePaymentConfirmationTest { + public: + SecurePaymentConfirmationSynchronousDestructionTest() = default; + ~SecurePaymentConfirmationSynchronousDestructionTest() override = default; + + void OnUIDisplayed() override { + SecurePaymentConfirmationTest::OnUIDisplayed(); + GetActiveWebContents()->Close(); + } +}; + +IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationSynchronousDestructionTest, + CloseTabDuringUIDisplayed) { + test_controller()->SetHasAuthenticator(true); + NavigateTo("a.com", "/secure_payment_confirmation.html"); + std::vector<uint8_t> credential_id = {'c', 'r', 'e', 'd'}; + std::vector<uint8_t> user_id = {'u', 's', 'e', 'r'}; + webdata_services::WebDataServiceWrapperFactory:: + GetWebPaymentsWebDataServiceForBrowserContext( + GetActiveWebContents()->GetBrowserContext(), + ServiceAccessType::EXPLICIT_ACCESS) + ->AddSecurePaymentConfirmationCredential( + std::make_unique<SecurePaymentConfirmationCredential>( + std::move(credential_id), "a.com", std::move(user_id)), + base::BindOnce( + &SecurePaymentConfirmationTest::OnWebDataServiceRequestDone, + weak_ptr_factory_.GetWeakPtr())); + + ResetEventWaiterForSingleEvent(TestEvent::kUIDisplayed); + ExecuteScriptAsync(GetActiveWebContents(), + "getSecurePaymentConfirmationStatus()"); + WaitForObservedEvent(); + + // The WebContents and PaymentRequest were closed during OnUIDisplayed(), + // and SetupModelAndShowDialogIfApplicable() returned safely without a UAF. +} + #if !BUILDFLAG(IS_ANDROID) // Intentionally do not enable the "SecurePaymentConfirmation" Blink runtime // feature or the browser-side Finch flag. diff --git a/components/payments/content/payment_request.cc b/components/payments/content/payment_request.cc index 2b1b2821..bbb4dac 100644 --- a/components/payments/content/payment_request.cc +++ b/components/payments/content/payment_request.cc @@ -590,7 +590,13 @@ bool is_resolving_promise_passed_into_show_method = !spec_->IsInitialized(); + // spec_->UpdateWith() can synchronously trigger observers that destroy the + // payment window's WebContents and delete `this`. + auto weak_this = weak_ptr_factory_.GetWeakPtr(); spec_->UpdateWith(std::move(details)); + if (!weak_this) { + return; + } if (is_resolving_promise_passed_into_show_method) { DCHECK(spec_->details().total); diff --git a/components/payments/content/payment_request_spec.cc b/components/payments/content/payment_request_spec.cc index 86e6e606..58ba3b0 100644 --- a/components/payments/content/payment_request_spec.cc +++ b/components/payments/content/payment_request_spec.cc @@ -214,8 +214,14 @@ NotifyOnSpecUpdated(); + // NotifyInitialized() can synchronously trigger observers that destroy the + // payment window's WebContents and delete `this`. + auto weak_this = weak_ptr_factory_.GetWeakPtr(); if (is_initialization) NotifyInitialized(); + if (!weak_this) { + return; + } current_update_reason_ = UpdateReason::NONE; } diff --git a/components/payments/content/payment_request_spec_unittest.cc b/components/payments/content/payment_request_spec_unittest.cc index 1e84b12..63400cc 100644 --- a/components/payments/content/payment_request_spec_unittest.cc +++ b/components/payments/content/payment_request_spec_unittest.cc @@ -10,6 +10,7 @@ #include "base/memory/weak_ptr.h" #include "base/strings/utf_string_conversions.h" #include "base/test/scoped_feature_list.h" +#include "components/payments/content/initialization_task.h" #include "components/strings/grit/components_strings.h" #include "content/public/common/content_features.h" #include "testing/gmock/include/gmock/gmock.h" @@ -47,6 +48,7 @@ } PaymentRequestSpec* spec() { return spec_.get(); } + void ResetSpec() { spec_.reset(); } private: std::unique_ptr<PaymentRequestSpec> spec_; @@ -312,4 +314,29 @@ EXPECT_TRUE(spec()->has_payer_error()); } + +class PaymentRequestSpecSynchronousDestructionTest + : public PaymentRequestSpecTest, + public InitializationTask::Observer { + public: + // InitializationTask::Observer: + void OnInitialized(InitializationTask* initialization_task) override { + ResetSpec(); + } +}; + +TEST_F(PaymentRequestSpecSynchronousDestructionTest, RecomputeSpecForDetails) { + RecreateSpecWithOptionsAndDetails(mojom::PaymentOptions::New(), + mojom::PaymentDetails::New()); + + spec()->AddInitializationObserver(this); + spec()->StartWaitingForUpdateWith( + PaymentRequestSpec::UpdateReason::INITIAL_PAYMENT_DETAILS); + spec()->RecomputeSpecForDetails(); + + // RecomputeSpecForDetails will have synchronously torn down the spec object, + // but should not cause a UAF. + EXPECT_FALSE(spec()); +} + } // namespace payments diff --git a/components/payments/content/payment_request_state.cc b/components/payments/content/payment_request_state.cc index c6bb3ae..f88dcff3 100644 --- a/components/payments/content/payment_request_state.cc +++ b/components/payments/content/payment_request_state.cc @@ -218,7 +218,14 @@ [](const auto& app) { return app->HasEnrolledInstrument(); }); are_requested_methods_supported_ |= !available_apps_.empty(); NotifyOnGetAllPaymentAppsFinished(); + + // NotifyInitialized() can synchronously trigger observers that destroy the + // payment window's WebContents and delete `this`. + auto weak_this = weak_ptr_factory_.GetWeakPtr(); NotifyInitialized(); + if (!weak_this) { + return; + } // Fulfill the pending CanMakePayment call. if (can_make_payment_callback_) diff --git a/components/payments/content/payment_request_state_unittest.cc b/components/payments/content/payment_request_state_unittest.cc index 32061ce1..ebc0fe5 100644 --- a/components/payments/content/payment_request_state_unittest.cc +++ b/components/payments/content/payment_request_state_unittest.cc @@ -20,6 +20,7 @@ #include "components/autofill/core/browser/data_model/addresses/autofill_profile.h" #include "components/autofill/core/browser/data_model/addresses/autofill_profile_test_api.h" #include "components/autofill/core/browser/test_utils/autofill_test_utils.h" +#include "components/payments/content/initialization_task.h" #include "components/payments/content/payment_app_factory.h" #include "components/payments/content/payment_app_service.h" #include "components/payments/content/payment_request_spec.h" @@ -637,5 +638,49 @@ EXPECT_FALSE(response().is_null()); }
Regression Test / PoC
diff --git a/chrome/browser/payments/secure_payment_confirmation_browsertest.cc b/chrome/browser/payments/secure_payment_confirmation_browsertest.cc
index 2e52323..c5ce281 100644
--- a/chrome/browser/payments/secure_payment_confirmation_browsertest.cc
+++ b/chrome/browser/payments/secure_payment_confirmation_browsertest.cc
@@ -523,6 +523,44 @@
test_controller()->CloseDialog();
}
+class SecurePaymentConfirmationSynchronousDestructionTest
+ : public SecurePaymentConfirmationTest {
+ public:
+ SecurePaymentConfirmationSynchronousDestructionTest() = default;
+ ~SecurePaymentConfirmationSynchronousDestructionTest() override = default;
+
+ void OnUIDisplayed() override {
+ SecurePaymentConfirmationTest::OnUIDisplayed();
+ GetActiveWebContents()->Close();
+ }
+};
+
+IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationSynchronousDestructionTest,
+ CloseTabDuringUIDisplayed) {
+ test_controller()->SetHasAuthenticator(true);
+ NavigateTo("a.com", "/secure_payment_confirmation.html");
+ std::vector<uint8_t> credential_id = {'c', 'r', 'e', 'd'};
+ std::vector<uint8_t> user_id = {'u', 's', 'e', 'r'};
+ webdata_services::WebDataServiceWrapperFactory::
+ GetWebPaymentsWebDataServiceForBrowserContext(
+ GetActiveWebContents()->GetBrowserContext(),
+ ServiceAccessType::EXPLICIT_ACCESS)
+ ->AddSecurePaymentConfirmationCredential(
+ std::make_unique<SecurePaymentConfirmationCredential>(
+ std::move(credential_id), "a.com", std::move(user_id)),
+ base::BindOnce(
+ &SecurePaymentConfirmationTest::OnWebDataServiceRequestDone,
+ weak_ptr_factory_.GetWeakPtr()));
+
+ ResetEventWaiterForSingleEvent(TestEvent::kUIDisplayed);
+ ExecuteScriptAsync(GetActiveWebContents(),
+ "getSecurePaymentConfirmationStatus()");
+ WaitForObservedEvent();
+
+ // The WebContents and PaymentRequest were closed during OnUIDisplayed(),
+ // and SetupModelAndShowDialogIfApplicable() returned safely without a UAF.
+}
+
#if !BUILDFLAG(IS_ANDROID)
// Intentionally do not enable the "SecurePaymentConfirmation" Blink runtime
// feature or the browser-side Finch flag.
diff --git a/components/payments/content/payment_request_spec_unittest.cc b/components/payments/content/payment_request_spec_unittest.cc
index 1e84b12..63400cc 100644
--- a/components/payments/content/payment_request_spec_unittest.cc
+++ b/components/payments/content/payment_request_spec_unittest.cc
@@ -10,6 +10,7 @@
#include "base/memory/weak_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
+#include "components/payments/content/initialization_task.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/common/content_features.h"
#include "testing/gmock/include/gmock/gmock.h"
@@ -47,6 +48,7 @@
}
PaymentRequestSpec* spec() { return spec_.get(); }
+ void ResetSpec() { spec_.reset(); }
private:
std::unique_ptr<PaymentRequestSpec> spec_;
@@ -312,4 +314,29 @@
EXPECT_TRUE(spec()->has_payer_error());
}
+
+class PaymentRequestSpecSynchronousDestructionTest
+ : public PaymentRequestSpecTest,
+ public InitializationTask::Observer {
+ public:
+ // InitializationTask::Observer:
+ void OnInitialized(InitializationTask* initialization_task) override {
+ ResetSpec();
+ }
+};
+
+TEST_F(PaymentRequestSpecSynchronousDestructionTest, RecomputeSpecForDetails) {
+ RecreateSpecWithOptionsAndDetails(mojom::PaymentOptions::New(),
+ mojom::PaymentDetails::New());
+
+ spec()->AddInitializationObserver(this);
+ spec()->StartWaitingForUpdateWith(
+ PaymentRequestSpec::UpdateReason::INITIAL_PAYMENT_DETAILS);
+ spec()->RecomputeSpecForDetails();
+
+ // RecomputeSpecForDetails will have synchronously torn down the spec object,
+ // but should not cause a UAF.
+ EXPECT_FALSE(spec());
+}
+
} // namespace payments
diff --git a/components/payments/content/payment_request_state_unittest.cc b/components/payments/content/payment_request_state_unittest.cc
index 32061ce1..ebc0fe5 100644
--- a/components/payments/content/payment_request_state_unittest.cc
+++ b/components/payments/content/payment_request_state_unittest.cc
@@ -20,6 +20,7 @@
#include "components/autofill/core/browser/data_model/addresses/autofill_profile.h"
#include "components/autofill/core/browser/data_model/addresses/autofill_profile_test_api.h"
#include "components/autofill/core/browser/test_utils/autofill_test_utils.h"
+#include "components/payments/content/initialization_task.h"
#include "components/payments/content/payment_app_factory.h"
#include "components/payments/content/payment_app_service.h"
#include "components/payments/content/payment_request_spec.h"
@@ -637,5 +638,49 @@
EXPECT_FALSE(response().is_null());
}
+class PaymentRequestStateSynchronousDestructionTest
+ : public PaymentRequestStateTest,
+ public InitializationTask::Observer {
+ public:
+ // InitializationTask::Observer:
+ void OnInitialized(InitializationTask* initialization_task) override {
+ state_.reset();
+ if (on_initialized_) {
+ std::move(on_initialized_).Run();
+ }
+ }
+
+ base::OnceClosure on_initialized_;
+};
+
+TEST_F(PaymentRequestStateSynchronousDestructionTest,
+ OnDoneCreatingPaymentApps) {
+ auto app_service = std::make_unique<PaymentAppService>(&context_);
+ app_service->AddFactoryForTesting(
+ std::make_unique<TestAppFactory>("https://example.test"));
+
+ base::RunLoop run_loop;
+ on_initialized_ = run_loop.QuitClosure();
+
+ RecreateState(mojom::PaymentOptions::New(), CreateDefaultDetails(),
+ GetMethodDataForUrlMethod("https://example.test"),
+ std::move(app_service));
+
+ // PaymentRequestState's constructor in RecreateState immediately calls
+ // app_service_->Create(), which kicks off payment app factories before
+ // callers can register as an InitializationTask::Observer. We rely on the
+ // fact that default payment app factories in PaymentAppService complete
+ // asynchronously on the message loop, so will not complete until we yield the
+ // run loop below.
+ state()->AddInitializationObserver(this);
+
+ run_loop.Run();
+
+ // OnDoneCreatingPaymentApps should have run and called the initialization
+ // observers, which then synchronously deleted the state. This should not
+ // cause a UAF.
+ EXPECT_FALSE(state_);
+}
+
} // namespace
} // namespace payments
Original Bug Report
Potential Browser-Process Use-After-Free in PaymentRequestState::OnDoneCreatingPaymentApps
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 potential Use-After-Free (UAF) vulnerability exists in the browser process during the completion of payment app discovery. Under certain conditions, notifying observers synchronously triggers a fullscreen exit and WebContents destruction, which destroys the PaymentRequest object and frees both SecurePaymentConfirmationController and PaymentRequestState on the stack. Subsequent unwinding of the execution flow leads to multiple potential Use-After-Free reads and callback execution on freed memory.
Affected files:
components/payments/content/payment_request_state.cccomponents/payments/content/secure_payment_confirmation_controller.cccomponents/payments/content/initialization_task.cc
Estimated timestamp from git blame: 2020-08-27
Root Cause Analysis
In PaymentRequestState::OnDoneCreatingPaymentApps (components/payments/content/payment_request_state.cc), NotifyInitialized() is called to notify initialization observers:
NotifyOnGetAllPaymentAppsFinished();
NotifyInitialized(); // Can synchronously free |this|
// Fulfill the pending CanMakePayment call.
if (can_make_payment_callback_)
std::move(can_make_payment_callback_).Run(GetCanMakePaymentValue());
During the execution of NotifyInitialized(), the registered SecurePaymentConfirmationController observer receives the notification and triggers SetupModelAndShowDialogIfApplicable(), which shows a web-modal dialog via constrained_window::ShowWebModalDialogViews().
When blocking web contents for the modal dialog, BrowserWindowModalDialogDelegate::SetWebContentsBlocked() detects if the tab is in HTML5 content fullscreen and synchronously requests exiting fullscreen by calling web_contents->ExitFullscreen(true) to ensure the user has full context for making a security decision. If exiting fullscreen synchronously destroys the initiating WebContents (for example, due to fullscreen-exit re-entrancy or nested loops), the parent PaymentRequest (which is a DocumentService) will be destroyed. This synchronously resets the dialog controller (spc_dialog_.reset()) and deletes the PaymentRequestState (state_).
As the synchronous call stack unwinds:
- UAF in Controller:
SecurePaymentConfirmationController::SetupModelAndShowDialogIfApplicableattempts to access its freed state (request_->spc_transaction_mode()). - UAF in InitializationTask:
InitializationTask::NotifyInitializedattempts to iterate over its freedobservers_list. - UAF in PaymentRequestState:
PaymentRequestState::OnDoneCreatingPaymentAppsattempts to access and execute its pending callbacks (such ascan_make_payment_callback_), which can result in an arbitrary indirect call in the unsandboxed browser process.
Potential Trigger Path
- Place the tab into HTML5 content fullscreen via
element.requestFullscreen(). - Call
PaymentRequest.show()with asecure-payment-confirmationpayment method. - Wait for the asynchronous payment app discovery to complete.
- Upon completion,
PaymentRequestState::OnDoneCreatingPaymentApps()executes, callingNotifyInitialized(), which shows the SPC modal dialog and triggersExitFullscreen(true). - Trigger re-entrant/synchronous destruction of the
WebContentsduring the fullscreen-exit state transition, causing both the controller and state object to be freed while they are still on the stack.
Note: The steps and behavior described above represent a potential analysis of the code flow; our tooling agent does not have the ability to run code or verify this via a live proof-of-concept.
Suggested Fix
To mitigate this potential issue, a base::WeakPtr self-liveness check should be introduced in PaymentRequestState::OnDoneCreatingPaymentApps() after calling NotifyInitialized(). If the state is destroyed, it should return immediately:
base::WeakPtr<PaymentRequestState> weak_this = weak_ptr_factory_.GetWeakPtr();
NotifyInitialized();
if (!weak_this)
return;
Additionally, the SPC controller should be hardened with weak_ptr checks during dialog setup and after returning from showing the dialog to prevent use of a freed SecurePaymentConfirmationController or request_ pointer.
Evaluated with Chrome root at commit: 94d9235ebe3b7276e5284f0dc5d55577ff949908
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.