From 2d0cabefb30bf2805a5697105a3270a60f0efd8a Mon Sep 17 00:00:00 2001 From: vuckro Date: Wed, 10 Jun 2026 12:35:53 +0200 Subject: [PATCH 1/2] fix(checkout): guard process_checkout() against missing cart and invalid gateway Two reachable fatals in Checkout::process_checkout() on the synchronous finalize path: - `$payment->get_meta('wu_original_cart')` returns its `false` default for any payment not created by the normal checkout flow (webhook/admin/register-API payments), after which `$this->order->set_membership(...)` calls a method on a bool. Sibling consumers (Discount_Code_Manager, Base_Stripe_Gateway) already validate this meta with `is_a(..., Cart::class)`; process_checkout did not. Now bails with a clean WP_Error. - The `elseif ($gateway->get_id() === 'free')` branch dereferenced `$gateway` which is `false` when an unknown `gateway` request value is supplied; guarded so it falls through to the existing "gateway not registered" error. Both are robustness/DoS fixes; no behavior change for valid checkouts. Co-Authored-By: Claude Fable 5 --- inc/checkout/class-checkout.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/inc/checkout/class-checkout.php b/inc/checkout/class-checkout.php index 9a0cc4a7..e4166567 100644 --- a/inc/checkout/class-checkout.php +++ b/inc/checkout/class-checkout.php @@ -3039,6 +3039,20 @@ public function process_checkout() { * It ensure that the cart is the same used in beginning of the process. */ $this->order = $payment->get_meta('wu_original_cart'); + + /* + * The original cart is only stored for payments created through the + * normal checkout flow (process_order). Payments created elsewhere + * (webhooks, admin, the register API) have no 'wu_original_cart' meta, + * so get_meta() returns its `false` default. Bail cleanly instead of + * fatally calling a method on a boolean. + */ + if ( ! $this->order instanceof \WP_Ultimo\Checkout\Cart) { + $this->errors = new \WP_Error('no-cart', __('This checkout session has expired or cannot be resumed. Please start over.', 'ultimate-multisite')); + + return false; + } + $this->order->set_membership($membership); $this->order->set_customer($customer); $this->order->set_payment($payment); @@ -3058,7 +3072,7 @@ public function process_checkout() { $gateway = wu_get_gateway($payment->get_gateway()); } elseif ($this->order->should_collect_payment() === false) { $gateway = wu_get_gateway('free'); - } elseif ($gateway->get_id() === 'free') { + } elseif ($gateway && $gateway->get_id() === 'free') { $this->errors = new \WP_Error('no-gateway', __('Payment gateway not registered.', 'ultimate-multisite')); return false; From fad78daafb92cecabd62d069a00813ef90037080 Mon Sep 17 00:00:00 2001 From: vuckro Date: Wed, 10 Jun 2026 22:02:45 +0200 Subject: [PATCH 2/2] style(checkout): parenthesize instanceof guard for readability (CodeRabbit nit) Co-Authored-By: Claude Fable 5 --- inc/checkout/class-checkout.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/checkout/class-checkout.php b/inc/checkout/class-checkout.php index e4166567..cc5aa75c 100644 --- a/inc/checkout/class-checkout.php +++ b/inc/checkout/class-checkout.php @@ -3047,7 +3047,7 @@ public function process_checkout() { * so get_meta() returns its `false` default. Bail cleanly instead of * fatally calling a method on a boolean. */ - if ( ! $this->order instanceof \WP_Ultimo\Checkout\Cart) { + if ( ! ($this->order instanceof \WP_Ultimo\Checkout\Cart)) { $this->errors = new \WP_Error('no-cart', __('This checkout session has expired or cannot be resumed. Please start over.', 'ultimate-multisite')); return false;