Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Payments
DescriptionUse after free in Payments
ComponentPayments
Bug ClassUAF
Tracker522568496
Fix commitb87b4b251a81 (chromium/src) +24/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-08

Changed Functions

FunctionChangeNotes
if
components/payments/content/secure_payment_confirmation_controller.cc
modified

Files Changed

  • components/payments/content/secure_payment_confirmation_controller.cc
From b87b4b251a81a1e17b4388046d56f93765b646b3 Mon Sep 17 00:00:00 2001
From: Xuehui Chen <xuehuichen@google.com>
Date: Fri, 19 Jun 2026 15:24:44 -0700
Subject: [PATCH] [SPC] Guard SPC Controller destruction during dialog close.

A `potential` UAF vulnerability exists in the browser process due to
synchronous reentrancy inside SecurePaymentConfirmationController.

This can happen if platform-specific synchronous activation dispatch
from native_widget_->Close() that reaches
ExtensionPopup::OnWidgetDestroying() inside the same call stack. Or
CloseDialog() itself trigger a controller destruction.

This change adds a guard to the `CloseDialog()` method that ensures the
call inside SecurePaymentConfirmationController will not continue to
prevent a memory crash.

Fixed: 522568496
Change-Id: Ifc0eaaebc42db33bb309e850881722df13cad35d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7959279
Reviewed-by: Slobodan Pejic <slobodan@chromium.org>
Reviewed-by: Darwin Yang <darwinyang@chromium.org>
Commit-Queue: Xuehui Chen <xuehuichen@google.com>
Cr-Commit-Position: refs/heads/main@{#1649831}
---

diff --git a/components/payments/content/secure_payment_confirmation_controller.cc b/components/payments/content/secure_payment_confirmation_controller.cc
index d0e4f98b..a232b58c 100644
--- a/components/payments/content/secure_payment_confirmation_controller.cc
+++ b/components/payments/content/secure_payment_confirmation_controller.cc
@@ -130,7 +130,15 @@
                                   SecurePaymentRequestOutcome::kAccept);
 
     is_dialog_showing_ = false;
+    // CloseDialog() -> Widget::Close() can potentially synchronously trigger
+    // activation observers that destroy the payment window's WebContents.
+    // This self weak pointer guard can prevent potential UAF if controller is
+    // synchronously deleted inside CloseDialog().
+    auto weak_this = weak_ptr_factory_.GetWeakPtr();
     CloseDialog();
+    if (!weak_this) {
+      return;
+    }
 
     if (!request_) {
       return;
@@ -163,7 +171,13 @@
                                 SecurePaymentRequestOutcome::kAnotherWay);
 
   is_dialog_showing_ = false;
+
+  // CloseDialog() can potentially delete `this`. See OnConfirm() above.
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
   CloseDialog();
+  if (!weak_this) {
+    return;
+  }
 
   if (!request_) {
     return;
@@ -189,7 +203,12 @@
   }
 
   is_dialog_showing_ = false;
+  // CloseDialog() can potentially delete `this`. See OnConfirm() above.
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
   CloseDialog();
+  if (!weak_this) {
+    return;
+  }
 
   if (!request_) {
     return;
@@ -209,7 +228,12 @@
   }
 
   is_dialog_showing_ = false;
+  // CloseDialog() can potentially delete `this`. See OnConfirm() above.
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
   CloseDialog();
+  if (!weak_this) {
+    return;
+  }
 
   if (!request_) {
     return;
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Browser-Process Use-After-Free in SecurePaymentConfirmationController

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 due to synchronous reentrancy inside SecurePaymentConfirmationController. Calling CloseDialog() on the controller can synchronously trigger window activation events that destroy the hosting WebContents, the DocumentService PaymentRequest, and the controller itself. Unwinding back to the controller’s handlers leads to a UAF read and copy of the freed controller’s request_ pointer.

Affected files:

  • components/payments/content/secure_payment_confirmation_controller.cc
  • chrome/browser/ui/views/payments/secure_payment_confirmation_dialog_view.cc
  • chrome/browser/payments/chrome_payment_request_delegate.cc
  • components/payments/content/payment_request.cc

Estimated timestamp from git blame: 2020-08-27

Root Cause Analysis

Four user-interaction handlers in SecurePaymentConfirmationController (OnCancel, OnAnotherWay, OnOptOut, and the error branch of OnConfirm) call CloseDialog() and then dereference/copy request_ (which is this->request_) without first verifying that the controller instance (this) is still alive:

// components/payments/content/secure_payment_confirmation_controller.cc:177-200
void SecurePaymentConfirmationController::OnCancel() {
  if (!is_dialog_showing_) { return; }
  ...
  is_dialog_showing_ = false;
  CloseDialog();                                   // [1] Can synchronously free `this`

  if (!request_) {                                 // [2] UAF read of this->request_
    return;
  }
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(&PaymentRequest::OnUserCancelled,
                                request_));        // [3] UAF copy of this->request_
}

CloseDialog() calls view_->HideDialog(), which invokes Widget::Close(). Under certain circumstances—such as when the dialog is hosted inside an extension popup—closing the widget shifts native activation. This activation shift can synchronously trigger ExtensionPopup::OnWidgetTreeActivated, which closes the popup window and destroys its hosting WebContents because the active web modal check is bypassed once the dialog is erased from the modal manager’s list.

Because PaymentRequest is a frame-bound Mojo DocumentService, the destruction of the WebContents results in ~PaymentRequest(), which calls ChromePaymentRequestDelegate::CloseDialog() and resets the std::unique_ptr<SecurePaymentConfirmationController> (spc_dialog_). This deletes the controller, leaving a dangling this on the stack when execution unwinds back to the handlers.

Potential Trigger Steps

Note: These are potential, suggested steps to reproduce the issue. Our analysis is based on static code tracing, and we do not have a running proof of concept.

  1. An extension popup embeds an attacker-influenced HTTPS <iframe> or page.
  2. The page calls the Secure Payment Confirmation API via new PaymentRequest(...) to display the SPC dialog on the popup’s WebContents.
  3. The user interacts with the dialog, clicking Cancel or Opt-out.
  4. The controller’s callback OnCancel() (or OnOptOut()) is triggered.
  5. The controller calls CloseDialog(), which synchronously initiates the popup widget destruction chain.
  6. The popup’s WebContents is destroyed, which synchronously deletes the PaymentRequest DocumentService.
  7. ~PaymentRequest() triggers ChromePaymentRequestDelegate::CloseDialog(), resetting the unique pointer holding the SecurePaymentConfirmationController.
  8. Execution returns to OnCancel(), resulting in a browser-process Use-After-Free when checking if (!request_) and copying request_.

Proposed Fix

To prevent synchronous destruction from causing a UAF, the handlers must use a self-WeakPtr guard to check if this remains alive after CloseDialog() is executed, matching in-tree precedents in sibling modules (such as AuthenticatorCommonImpl and DigitalIdentityRequestImpl).

void SecurePaymentConfirmationController::OnCancel() {
  if (!is_dialog_showing_) {
    return;
  }
  ...
  is_dialog_showing_ = false;

  base::WeakPtr<SecurePaymentConfirmationController> weak_this = GetWeakPtr();
  CloseDialog();
  if (!weak_this) {
    return;
  }

  if (!request_) {
    return;
  }
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(&PaymentRequest::OnUserCancelled, request_));
}

Evaluated with Chrome root at commit: b2fea2e31df308d0f04e4ae47def4c4f939ee141


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