CVE-2026-19175
Overview
Background
- `PaymentRequest` object graph
- In the browser process, a
PaymentRequestowns itsPaymentRequestSpec(spec_), itsPaymentRequestState, and UI controllers such asSecurePaymentConfirmationController. All of them are freed when the requestingWebContentsis destroyed. - Initialization observers
PaymentRequestSpecandPaymentRequestStateareInitializationTasks whoseNotifyInitialized()synchronously runsInitializationTask::Observer::OnInitialized. Those observers can proceed to show the payment UI.- Secure Payment Confirmation (SPC) modal and fullscreen exit
- Per the commit, showing the SPC dialog via
ShowWebModalDialogViews()callsExitFullscreen(true)synchronously if the tab is fullscreen. On Windows and macOS this can pump a nested message loop or pending window messages, during which theWebContentsmay be destroyed. - `base::WeakPtr` self-liveness guard
- taking
weak_ptr_factory_.GetWeakPtr()before a re-entrant call and returning if it has been invalidated afterwards.
Root Cause Analysis
Several Payments methods made synchronous calls that can, through observers, end up showing the SPC modal. Each of them then kept using this. Those calls are NotifyInitialized() in PaymentRequestState::OnDoneCreatingPaymentApps and PaymentRequestSpec::RecomputeSpecForDetails, spec_->UpdateWith() in PaymentRequest::UpdateWith, and view_->ShowDialog() in SecurePaymentConfirmationController::SetupModelAndShowDialogIfApplicable. Showing that modal can exit fullscreen and spin a nested message loop that destroys the WebContents, freeing PaymentRequest and everything it owns while these frames are still on the stack. Execution then resumed in freed objects, for example by assigning current_update_reason_, running can_make_payment_callback_, reading spec_->details(), or continuing with the auto-accept/auto-reject logic in the SPC controller. Nothing checked that the object was still alive after a call that could trigger arbitrary teardown.
The fix takes a WeakPtr before each re-entrant call and returns immediately if it has been invalidated, so no member is touched after destruction.
NotifyInitialized, UpdateWith, ShowDialog) were treated as non-reentrant, but showing the SPC modal can pump messages and delete the whole PaymentRequest graph. The fix adds WeakPtr liveness checks right after each such call.Attack Path
- Set up an SPC request
A page with an enrolled SPC credential (the browser test adds one for
a.com) callsPaymentRequestwith Secure Payment Confirmation, optionally while it is fullscreen. - Reach a re-entrant call
Initialization finishes (
OnDoneCreatingPaymentAppsorRecomputeSpecForDetailscallingNotifyInitialized()), or the page resolvesshow()details (PaymentRequest::UpdateWith). This leads toSetupModelAndShowDialogIfApplicablecallingview_->ShowDialog(). - Teardown inside the call
Showing the modal calls
ExitFullscreen(true), and the resulting nested message processing on Windows or macOS destroys theWebContents, for example a pending tab close. The test simulates this by callingWebContents::Close()fromOnUIDisplayed. - Use after free
Before the patch, each caller resumed and dereferenced members of the now-freed
PaymentRequest,PaymentRequestSpec,PaymentRequestStateorSecurePaymentConfirmationControllerin the browser process. Turning this into a controlled corruption primitive is (inferred).
Impact Assessment
WebContents is destroyed during the synchronous dialog-show path. The commit attributes that destruction to the fullscreen exit pumping messages on Windows and macOS, so it is likely timing- and platform-dependent; for example the page must be fullscreen, which normally requires a user gesture (inferred). The advisory states only “Use after free in Payments” and does not report in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/payments/content/payment_request.cc |
modified | |
ifcomponents/payments/content/payment_request_spec.cc |
modified | |
ifcomponents/payments/content/payment_request_state.cc |
modified | |
ifcomponents/payments/content/secure_payment_confirmation_controller.cc |
modified | |
SecurePaymentConfirmationSynchronousDestructionTestchrome/browser/payments/secure_payment_confirmation_browsertest.cc |
modified | |
PaymentRequestSpecSynchronousDestructionTestcomponents/payments/content/payment_request_spec_unittest.cc |
modified | |
TEST_Fcomponents/payments/content/payment_request_spec_unittest.cc |
modified | |
PaymentRequestStateSynchronousDestructionTestcomponents/payments/content/payment_request_state_unittest.cc |
modified | |
ifcomponents/payments/content/payment_request_state_unittest.cc |
modified |
Files Changed
components/payments/content/payment_request_spec_unittest.cccomponents/payments/content/secure_payment_confirmation_controller.cccomponents/payments/content/payment_request_state_unittest.cccomponents/payments/content/payment_request.ccchrome/browser/payments/secure_payment_confirmation_browsertest.cccomponents/payments/content/payment_request_state.cccomponents/payments/content/payment_request_spec.cc
Audit Directions
- Other show-UI paths that can exit fullscreenAudit callers of
ShowWebModalDialogViews()and otherExitFullscreen(true)call sites in the browser, such as WebAuthn, permission prompts, autofill and FedCM dialogs. Look for code that continues to usethisafter showing a modal synchronously. - Nested message loops on Windows and macOSSearch for calls that may pump messages (window activation, fullscreen transitions,
SendMessage-based Win32 APIs,NSAppmodal sessions) inside browser-side objects owned byWebContents. Verify that the code after those calls tolerates the tab being destroyed.
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/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_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/secure_payment_confirmation_controller.cc b/components/payments/content/secure_payment_confirmation_controller.cc index a232b58c..052e5a4e 100644 --- a/components/payments/content/secure_payment_confirmation_controller.cc +++ b/components/payments/content/secure_payment_confirmation_controller.cc @@ -373,6 +373,9 @@ view_ = SecurePaymentConfirmationView::Create( request_->state()->GetPaymentRequestDelegate()->GetPaymentUIObserver()); + // view_->ShowDialog() can potentially trigger observers that destroy the + // payment window's WebContents and delete `this`. + auto weak_this = weak_ptr_factory_.GetWeakPtr(); view_->ShowDialog( request_->web_contents(), model_.GetWeakPtr(), base::BindOnce(&SecurePaymentConfirmationController::OnConfirm, @@ -383,6 +386,9 @@ weak_ptr_factory_.GetWeakPtr()), base::BindOnce(&SecurePaymentConfirmationController::OnOptOut, weak_ptr_factory_.GetWeakPtr())); + if (!weak_this) { + return; + } // For automated testing, SPC can be placed in an 'autoaccept' or // 'autoreject' mode, where the dialog should immediately be 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
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.