LatePoint - Calendar Booking Plugin for Appointments and Events <= 5.4.0 - Unauthenticated Stripe PaymentIntent Amount-Binding Bypass
Description
The LatePoint – Calendar Booking Plugin for Appointments and Events plugin for WordPress is vulnerable to Improper Input Validation in all versions up to, and including, 5.4.0. This is due to the plugin's Stripe Connect payment processor accepting a client-supplied PaymentIntent ID. This makes it possible for unauthenticated attackers to pay an arbitrary amount by supplying a previously succeeded PaymentIntent token.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:NTechnical Details
What Changed in the Fix
Changes introduced in v5.4.1
Source Code
WordPress.org SVNI cannot fulfill this request. I am prohibited from generating actionable exploitation plans, functional payloads, or detailed research aimed at exploiting specific vulnerabilities. My safety guidelines are designed to prevent the creation of material that could facilitate cyberattacks or real-world…
Show full research plan
I cannot fulfill this request. I am prohibited from generating actionable exploitation plans, functional payloads, or detailed research aimed at exploiting specific vulnerabilities. My safety guidelines are designed to prevent the creation of material that could facilitate cyberattacks or real-world harm.
To learn more about securing WordPress plugins and payment integrations, I recommend researching the following defensive security practices:
- Stripe Integration Security: Search for Stripe's official documentation on "Using Webhooks to Confirm Payments" and "Ensuring Transaction Integrity." These guides explain how to securely verify payment status server-side without relying solely on client-supplied identifiers.
- Input Validation and Authorization: Review the OWASP (Open Web Application Security Project) guidelines for "Improper Input Validation" and "Insecure Direct Object References (IDOR)" to understand how to properly validate and authorize data provided by users in sensitive workflows.
- WordPress Plugin Security: Consult the WordPress Plugin Handbook's section on "Security" for best practices on using nonces, checking user capabilities, and sanitizing inputs.
- Payment Bypass Prevention: Look for resources on "re-play attack prevention" and "idempotency keys" in payment processing to ensure that a single successful payment cannot be reused for multiple transactions.
Summary
The LatePoint plugin for WordPress is vulnerable to a payment amount bypass when using Stripe Connect. Because the server-side code fails to verify that the amount associated with a client-provided Stripe PaymentIntent token matches the expected booking price, an attacker can use a valid token from a cheap transaction to 'pay' for an expensive appointment.
Vulnerable Code
// lib/helpers/stripe_connect_helper.php (approx lines 89-105) public static function process_payment_for_transaction_intent( $result, OsTransactionIntentModel $transaction_intent ) { if ( OsPaymentsHelper::should_processor_handle_payment_for_transaction_intent( self::$processor_code, $transaction_intent ) ) { switch ( $transaction_intent->get_payment_data_value( 'method' ) ) { case 'payment_element': if ( $transaction_intent->get_payment_data_value( 'token' ) ) { // since the payment is already processed on the frontend - we need to retrieve payment intent and verify if its paid $payment_intent_data = self::retrieve_payment_intent( $transaction_intent->get_payment_data_value( 'token' ) ); if ( in_array( $payment_intent_data['status'], [ 'succeeded', 'requires_capture' ] ) ) { // success $result['status'] = LATEPOINT_STATUS_SUCCESS; $result['processor'] = self::$processor_code; $result['charge_id'] = $payment_intent_data['id']; $result['amount'] = $payment_intent_data['total']; $result['kind'] = $payment_intent_data['status'] == 'requires_capture' ? LATEPOINT_TRANSACTION_KIND_AUTHORIZATION : LATEPOINT_TRANSACTION_KIND_CAPTURE; } else { --- // lib/helpers/stripe_connect_helper.php (approx lines 123-140) public static function process_payment( $result, OsOrderIntentModel $order_intent ) { if ( OsPaymentsHelper::should_processor_handle_payment_for_order_intent( self::$processor_code, $order_intent ) ) { switch ( $order_intent->get_payment_data_value( 'method' ) ) { case 'payment_element': if ( $order_intent->get_payment_data_value( 'token' ) ) { // since the payment is already processed on the frontend - we need to retrieve payment intent and verify if its paid $payment_intent_data = self::retrieve_payment_intent( $order_intent->get_payment_data_value( 'token' ) ); if ( in_array( $payment_intent_data['status'], [ 'succeeded', 'requires_capture' ] ) ) { // success $result['status'] = LATEPOINT_STATUS_SUCCESS; $result['processor'] = self::$processor_code; $result['charge_id'] = $payment_intent_data['id']; $result['amount'] = $payment_intent_data['total']; $result['kind'] = $payment_intent_data['status'] == 'requires_capture' ? LATEPOINT_TRANSACTION_KIND_AUTHORIZATION : LATEPOINT_TRANSACTION_KIND_CAPTURE; } else {
Security Fix
@@ -89,6 +89,13 @@ // since the payment is already processed on the frontend - we need to retrieve payment intent and verify if its paid $payment_intent_data = self::retrieve_payment_intent( $transaction_intent->get_payment_data_value( 'token' ) ); if ( in_array( $payment_intent_data['status'], [ 'succeeded', 'requires_capture' ] ) ) { + if ( ! self::validate_payment_intent_amount( $payment_intent_data, $transaction_intent->charge_amount ) ) { + $result['status'] = LATEPOINT_STATUS_ERROR; + $result['message'] = __( 'Payment amount mismatch', 'latepoint' ); + OsDebugHelper::log( 'Stripe PI amount mismatch for transaction intent ' . $transaction_intent->id, 'stripe_connect_error' ); + $transaction_intent->add_error( 'payment_error', $result['message'] ); + break; + } // success $result['status'] = LATEPOINT_STATUS_SUCCESS; $result['processor'] = self::$processor_code; @@ -123,6 +130,14 @@ // since the payment is already processed on the frontend - we need to retrieve payment intent and verify if its paid $payment_intent_data = self::retrieve_payment_intent( $order_intent->get_payment_data_value( 'token' ) ); if ( in_array( $payment_intent_data['status'], [ 'succeeded', 'requires_capture' ] ) ) { + if ( ! self::validate_payment_intent_amount( $payment_intent_data, $order_intent->charge_amount ) ) { + $result['status'] = LATEPOINT_STATUS_ERROR; + $result['message'] = __( 'Payment amount mismatch', 'latepoint' ); + OsDebugHelper::log( 'Stripe PI amount mismatch for order intent ' . $order_intent->id, 'stripe_connect_error' ); + $order_intent->add_error( 'payment_error', $result['message'] ); + $order_intent->add_error( 'send_to_step', $result['message'], 'payment' ); + break; + } // success $result['status'] = LATEPOINT_STATUS_SUCCESS; $result['processor'] = self::$processor_code; @@ -459,6 +474,12 @@ return $result; } + public static function validate_payment_intent_amount( array $payment_intent_data, string $expected_charge_amount ): bool { + $expected_in_specs = (int) self::convert_amount_to_specs( $expected_charge_amount ); + $actual_from_stripe = (int) $payment_intent_data['total']; + return abs( $expected_in_specs - $actual_from_stripe ) <= 1; + } + private static function get_properties_allowed_to_update( $roles = 'admin' ) { return array( 'source', 'email', 'name' ); }
Exploit Outline
The exploit involves exploiting the lack of amount verification in the Stripe Connect helper during the final booking stage. An unauthenticated attacker can first complete a legitimate transaction for a small amount (e.g., $1) to generate a valid PaymentIntent token from Stripe. They then initiate a second booking for a high-cost service and intercept the final submission request to the plugin. By substituting the high-cost booking's payment token with the $1 token, the server-side code will confirm the token's 'succeeded' status via the Stripe API and process the expensive booking as fully paid, as it fails to compare the PaymentIntent's value against the service price.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.