From 61bacc82cbade646eee68d7c50940f6a9ff1cc73 Mon Sep 17 00:00:00 2001 From: vuckro Date: Wed, 10 Jun 2026 11:43:42 +0200 Subject: [PATCH] fix(security): enforce payment ownership in status-polling AJAX endpoint ajax_check_payment_status() looked a payment up by its hash and then disclosed its status and called the gateway's verify_and_complete_payment() with no ownership check. The payment hash is a reversible identifier and the 'wu_payment_status_poll' nonce is a single static value localised into every checkout/thank-you page, so neither restricts which payment a caller may target. Any logged-in user could therefore poll arbitrary payments and trigger gateway verification on them. Require the current customer to own the payment (or be a network admin) before proceeding, returning the same generic "not found" response on mismatch to avoid leaking which payment ids exist. Co-Authored-By: Claude Fable 5 --- inc/managers/class-gateway-manager.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/inc/managers/class-gateway-manager.php b/inc/managers/class-gateway-manager.php index be74ab481..932980fb1 100644 --- a/inc/managers/class-gateway-manager.php +++ b/inc/managers/class-gateway-manager.php @@ -637,6 +637,22 @@ public function ajax_check_payment_status(): void { wp_send_json_error(['message' => __('Payment not found.', 'ultimate-multisite')]); } + /* + * Enforce ownership. The payment hash is a reversible identifier (not a + * secret), and the polling nonce is a static value localised into every + * checkout/thank-you page, so neither binds the request to a payment. + * Without this check any logged-in user could poll the status of — and + * trigger gateway verification on — arbitrary payments. Only the owning + * customer (or a network admin) may proceed. + */ + $current_customer = wu_get_current_customer(); + + $is_owner = $current_customer && (int) $payment->get_customer_id() === (int) $current_customer->get_id(); + + if ( ! $is_owner && ! current_user_can('manage_network')) { + wp_send_json_error(['message' => __('Payment not found.', 'ultimate-multisite')]); + } + // If already completed, return success if ($payment->get_status() === \WP_Ultimo\Database\Payments\Payment_Status::COMPLETED) { wp_send_json_success(