CVE-2026-32490

WP TripAdvisor Review Slider <= 14.1 - Authenticated (Subscriber+) Stored Cross-Site Scripting

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

Description

The WP TripAdvisor Review Slider plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 14.1 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with subscriber-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<=14.1
PublishedMarch 23, 2026
Last updatedMarch 26, 2026

What Changed in the Fix

Changes introduced in v14.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

### 1. Vulnerability Summary The **WP TripAdvisor Review Slider** plugin (versions <= 14.1) contains a **Stored Cross-Site Scripting (XSS)** vulnerability. The vulnerability arises from an AJAX endpoint (likely `wprev_tripadvisor_save_template` or `wprev_trip_save_template`) that allows authenticate…

Show full research plan

1. Vulnerability Summary

The WP TripAdvisor Review Slider plugin (versions <= 14.1) contains a Stored Cross-Site Scripting (XSS) vulnerability. The vulnerability arises from an AJAX endpoint (likely wprev_tripadvisor_save_template or wprev_trip_save_template) that allows authenticated users with at least Subscriber-level permissions to modify plugin template settings. Specifically, the field template_css (and potentially title or read_more_text) in the wptripadvisor_post_templates table is not sufficiently sanitized before being stored in the database, and is subsequently echoed onto public-facing pages via the [wptripadvisor_usetemplate] shortcode without proper output escaping.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: wprev_tripadvisor_save_template (Inferred from plugin slug and template table wptripadvisor_post_templates).
  • Vulnerable Parameter: template_css (stored in the wptripadvisor_post_templates table).
  • Authentication: Authenticated (Subscriber or higher).
  • Nonce: Required. The nonce action is randomnoncestring.
  • Preconditions:
    1. At least one template must exist in the database (usually created by an admin during initial setup).
    2. The shortcode [wptripadvisor_usetemplate] must be present on a public page to leak the required nonce.

3. Code Flow

  1. Input: A Subscriber sends a POST request to admin-ajax.php with the action wprev_tripadvisor_save_template.
  2. Processing: The AJAX handler (in WP_TripAdvisor_Review_Admin) verifies the nonce randomnoncestring. Crucially, it fails to perform a capability check (e.g., current_user_can('manage_options')).
  3. Storage: The handler takes the input (likely from a form_data string or direct POST params) and updates the wptripadvisor_post_templates table.
    • Reference: includes/class-wp-tripadvisor-review-slider.php defines the table schema, including template_css text NOT NULL.
  4. Public Output: A user visits a page containing the shortcode [wptripadvisor_usetemplate tid="1"].
  5. Rendering: public/class-wp-tripadvisor-review-slider-public.php calls wptripadvisor_usetemplate_func, which includes public/partials/wp-tripadvisor-review-slider-public-display.php.
  6. Sink: The display logic fetches the template_css from the database and echoes it inside a <style> tag or directly into the page without using wp_strip_all_tags or esc_html.

4. Nonce Acquisition Strategy

The plugin localizes the necessary nonce for public use, making it available to any logged-in user (including Subscribers) viewing a page where the plugin's shortcode is active.

  1. Identify Shortcode: The shortcode is [wptripadvisor_usetemplate tid="1"].
  2. Navigation: Navigate to a page where this shortcode is present.
  3. Variable Extraction: The plugin uses wp_localize_script in public/class-wp-tripadvisor-review-slider-public.php.
    • JS Object: window.wprevpublicjs_script_vars
    • Nonce Key: wpfb_nonce (Note: Verbatim from source, likely inherited from the developer's Facebook plugin).
  4. Action String: The nonce is created using wp_create_nonce('randomnoncestring').

Execution:

// Run in browser console on page with shortcode
let nonce = window.wprevpublicjs_script_vars?.wpfb_nonce;
console.log(nonce);

5. Exploitation Strategy

  1. Login as Subscriber: Obtain session cookies for a Subscriber-level user.
  2. Get Nonce: Navigate to a public page containing the TripAdvisor slider and extract the wpfb_nonce.
  3. Craft Payload: The goal is to inject a script into the template settings. Since template_css is often placed inside a <style> block, we will break out of it.
    • Payload: </style><script>alert(document.domain)</script>
  4. Trigger AJAX Request: Send a POST request to admin-ajax.php.

Request Details:

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=wprev_tripadvisor_save_template&
    nonce=[EXTRACTED_NONCE]&
    template_id=1&
    template_css=</style><script>alert(document.domain)</script>
    
    (Note: If the plugin expects form_data, the body would be: action=wprev_tripadvisor_save_template&nonce=[NONCE]&form_data=id%3D1%26template_css%3D%3C%2Fstyle%3E%3Cscript%3Ealert(1)%3C%2Fscript%3E)

6. Test Data Setup

  1. Administrator: Create a template via the plugin menu (WP TA Reviews > Templates). This creates ID 1 in wptripadvisor_post_templates.
  2. Administrator: Create a public Post or Page and insert the shortcode: [wptripadvisor_usetemplate tid="1"].
  3. Administrator: Create a user with the Subscriber role.

7. Expected Results

  • The AJAX request should return a successful response (likely a JSON 1 or a success message).
  • When any user (including an Admin) visits the page containing the shortcode, the browser will execute the injected script, and an alert box showing the domain will appear.

8. Verification Steps

  1. Database Check: Use WP-CLI to verify the stored payload.
    wp db query "SELECT template_css FROM wp_wptripadvisor_post_templates WHERE id=1;"
    
  2. Frontend Inspection: Check the HTML source of the page containing the shortcode.
    # Search for the injected payload in the page output
    http_request(url='http://localhost:8080/path-to-page/') 
    # Look for </style><script>alert(document.domain)</script>
    

9. Alternative Approaches

If wprev_tripadvisor_save_template is not the correct action name, check the admin/js/wptripadvisor_templates_posts_page.js file using the browser_eval tool to find the jQuery.post or jQuery.ajax call.

Other potential vulnerable fields in the same AJAX call:

  • title: May be reflected in the admin list or as a header.
  • read_more_text: Often echoed inside an <a> tag.
    • Payload: "> <img src=x onerror=alert(1)>
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP TripAdvisor Review Slider plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) in versions up to 14.1. This occurs because the plugin exposes a security nonce to all users on the frontend and fails to implement capability checks in its AJAX handlers, allowing authenticated users with Subscriber-level access to inject malicious scripts into review data or templates that are later displayed without proper output escaping.

Vulnerable Code

// public/class-wp-tripadvisor-review-slider-public.php line 135
wp_localize_script($this->_token."_plublic", 'wprevpublicjs_script_vars', 
						array(
						'wpfb_nonce'=> wp_create_nonce('randomnoncestring'),
						'wpfb_ajaxurl' => admin_url( 'admin-ajax.php' ),
						'wprevpluginsurl' => wprev_trip_plugin_url
						)
					);

---

// admin/class-wp-tripadvisor-review-slider-admin.php line 410
check_ajax_referer('randomnoncestring', 'wptripadvisor_nonce');

$postreviewarray = $_POST['postreviewarray'];
// ...
foreach($postreviewarray as $item) { //foreach element in $arr
	$pageid = $item['pageid'];
	$pagename = $item['pagename'];
	$created_time = $item['created_time'];
	$created_time_stamp = strtotime($created_time);
	$reviewer_name = $item['reviewer_name'];
	$reviewer_id = $item['reviewer_id'];
	$rating = $item['rating'];
	$review_text = $item['review_text'];

---

// admin/partials/review_list.php line 220
$html .= '<tr id="'.$reviewsrow->id.'">
		<th scope="col" class="manage-column"><a title="delete" alt="delete" href="'.$deleteurl.'">'.$deleteicon.'</a></th>
		<th scope="col" class="manage-column">'.$userpic.'</th>
		<th scope="col" class="manage-column">'.$reviewsrow->reviewer_name.'</th>
		<th scope="col" class="manage-column">'.$reviewsrow->rating.'</th>
		<th scope="col" class="manage-column">'.$revtitle.$reviewsrow->review_text.$mediahtml.'</th>
		<th scope="col" class="manage-column">'.$reviewsrow->created_time.'</th>
		<th scope="col" class="manage-column">'.$typecolumn.'</th>
	</tr>';

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-tripadvisor-review-slider/14.1/admin/class-wp-tripadvisor-review-slider-admin.php /home/deploy/wp-safety.org/data/plugin-versions/wp-tripadvisor-review-slider/14.2/admin/class-wp-tripadvisor-review-slider-admin.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-tripadvisor-review-slider/14.1/admin/class-wp-tripadvisor-review-slider-admin.php	2025-12-05 19:08:06.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-tripadvisor-review-slider/14.2/admin/class-wp-tripadvisor-review-slider-admin.php	2026-02-09 19:59:22.000000000 +0000
@@ -410,6 +410,12 @@
 		
 		check_ajax_referer('randomnoncestring', 'wptripadvisor_nonce');
 		
+		// SECURITY FIX: Verify user has permission to manage reviews
+		if (!current_user_can('manage_options')) {
+			wp_send_json_error('Insufficient permissions');
+			wp_die();
+		}
+		
 		$postreviewarray = $_POST['postreviewarray'];
 		
 		//var_dump($postreviewarray);
@@ -421,22 +427,26 @@
 		$stats = array();
 		
 		foreach($postreviewarray as $item) { //foreach element in $arr
-			$pageid = $item['pageid'];
-			$pagename = $item['pagename'];
-			$created_time = $item['created_time'];
+			// SECURITY FIX: Sanitize all input data
+			$pageid = sanitize_text_field($item['pageid']);
+			$pagename = sanitize_text_field($item['pagename']);
+			$created_time = sanitize_text_field($item['created_time']);
 			$created_time_stamp = strtotime($created_time);
-			$reviewer_name = $item['reviewer_name'];
-			$reviewer_id = $item['reviewer_id'];
-			$rating = $item['rating'];
-			$review_text = $item['review_text'];
+			$reviewer_name = sanitize_text_field($item['reviewer_name']);
+			$reviewer_id = sanitize_text_field($item['reviewer_id']);
+			$rating = intval($item['rating']);
+			$review_text = wp_kses_post($item['review_text']);
 			$review_length = str_word_count($review_text);
-			$rtype = $item['type'];
+			$rtype = sanitize_text_field($item['type']);
 			
 			//check to see if row is in db already
-			//$checkrow = $wpdb->get_row( "SELECT id FROM ".$table_name." WHERE created_time = '$created_time'" );
-			//$checkrow = $wpdb->get_var( 'SELECT id FROM '.$table_name.' WHERE reviewer_name = "'.$reviewer_name.'" AND (review_length = "'.$review_length.'" OR created_time_stamp = "'.$created_time_stamp.'")' );
-			
-			$checkrow = $wpdb->get_var( "SELECT id FROM ".$table_name." WHERE reviewer_name = '".$reviewer_name."' AND (review_length = '".$review_length."' OR created_time_stamp = '".$created_time_stamp."')" );
+			// SECURITY FIX: Use prepared statement to prevent SQL injection
+			$checkrow = $wpdb->get_var( $wpdb->prepare(
+				"SELECT id FROM ".$table_name." WHERE reviewer_name = %s AND (review_length = %d OR created_time_stamp = %d)",
+				$reviewer_name,
+				$review_length,
+				$created_time_stamp
+			) );
 			
 			//echo $wpdb->last_result;
 			//echo "<br>here<br>";
@@ -487,6 +497,12 @@
 		
 		check_ajax_referer('randomnoncestring', 'wptripadvisor_nonce');
 		
+		// SECURITY FIX: Verify user has permission to manage reviews
+		if (!current_user_can('manage_options')) {
+			wp_send_json_error('Insufficient permissions');
+			wp_die();
+		}
+		
 		$rid = intval($_POST['reviewid']);
 		$myaction = $_POST['myaction'];
 
@@ -570,6 +586,13 @@
 	//error_reporting(E_ALL);
 		
 		check_ajax_referer('randomnoncestring', 'wptripadvisor_nonce');
+		
+		// SECURITY FIX: Verify user has permission to manage reviews
+		if (!current_user_can('manage_options')) {
+			wp_send_json_error('Insufficient permissions');
+			wp_die();
+		}
+		
 		$filtertext = htmlentities($_POST['filtertext']);
 		$filterrating = htmlentities($_POST['filterrating']);
 		$filterrating = intval($filterrating);
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-tripadvisor-review-slider/14.1/admin/partials/review_list.php /home/deploy/wp-safety.org/data/plugin-versions/wp-tripadvisor-review-slider/14.2/admin/partials/review_list.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-tripadvisor-review-slider/14.1/admin/partials/review_list.php	2025-12-05 19:08:06.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-tripadvisor-review-slider/14.2/admin/partials/review_list.php	2026-02-09 19:59:22.000000000 +0000
@@ -182,16 +182,19 @@
 				
 				//user profile link
 				if( $reviewsrow->type=="TripAdvisor"){
-					$userpic = '<img style="-webkit-user-select: none;width: 50px;" src="'.$reviewsrow->userpic.'">';
+					// SECURITY FIX: Escape URL in src attribute
+					$userpic = '<img style="-webkit-user-select: none;width: 50px;" src="'.esc_url($reviewsrow->userpic).'">';
 					$editdellink = '';
 				}else {
-					$userpic = '<img style="-webkit-user-select: none;width: 50px;" src="'.$reviewsrow->userpic.'">';
+					// SECURITY FIX: Escape URL in src attribute
+					$userpic = '<img style="-webkit-user-select: none;width: 50px;" src="'.esc_url($reviewsrow->userpic).'">';
 					$editdellink = '<a title="Edit" href="'.$url_tempeditbtn.'"><span class="reveditbtn dashicons dashicons-edit"></span></a><span title="Delete" class="revdelbtn text_red dashicons dashicons-trash"></span>';
 					
 				}
 				$revtitle = '';
 				if($reviewsrow->review_title!=''){
-					$revtitle = '<b>'.$reviewsrow->review_title.'</b></br>';
+					// SECURITY FIX: Escape HTML in title
+					$revtitle = '<b>'.esc_html($reviewsrow->review_title).'</b></br>';
 				}
 				
 				$deleteurl = add_query_arg( 'deleterev', $reviewsrow->id,$currenturl );
@@ -213,18 +216,20 @@
 				
 	
 				// Build Type column with link if from_url exists
-				$typecolumn = $reviewsrow->type;
+				// SECURITY FIX: Escape HTML output
+				$typecolumn = esc_html($reviewsrow->type);
 				if(!empty($reviewsrow->from_url)){
-					$typecolumn = '<a href="'.esc_url($reviewsrow->from_url).'" target="_blank" rel="noopener noreferrer">'.$reviewsrow->type.'</a>';
+					$typecolumn = '<a href="'.esc_url($reviewsrow->from_url).'" target="_blank" rel="noopener noreferrer">'.esc_html($reviewsrow->type).'</a>';
 				}
 				
-				$html .= '<tr id="'.$reviewsrow->id.'">
+				// SECURITY FIX: Escape all output to prevent XSS
+				$html .= '<tr id="'.esc_attr($reviewsrow->id).'">
 						<th scope="col" class="manage-column"><a title="delete" alt="delete" href="'.$deleteurl.'">'.$deleteicon.'</a></th>
 						<th scope="col" class="manage-column">'.$userpic.'</th>
-						<th scope="col" class="manage-column">'.$reviewsrow->reviewer_name.'</th>
-						<th scope="col" class="manage-column">'.$reviewsrow->rating.'</th>
-						<th scope="col" class="manage-column">'.$revtitle.$reviewsrow->review_text.$mediahtml.'</th>
-						<th scope="col" class="manage-column">'.$reviewsrow->created_time.'</th>
+						<th scope="col" class="manage-column">'.esc_html($reviewsrow->reviewer_name).'</th>
+						<th scope="col" class="manage-column">'.esc_html($reviewsrow->rating).'</th>
+						<th scope="col" class="manage-column">'.$revtitle.wp_kses_post($reviewsrow->review_text).$mediahtml.'</th>
+						<th scope="col" class="manage-column">'.esc_html($reviewsrow->created_time).'</th>
 						<th scope="col" class="manage-column">'.$typecolumn.'</th>
 					</tr>';
 			}
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-tripadvisor-review-slider/14.1/public/class-wp-tripadvisor-review-slider-public.php /home/deploy/wp-safety.org/data/plugin-versions/wp-tripadvisor-review-slider/14.2/public/class-wp-tripadvisor-review-slider-public.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-tripadvisor-review-slider/14.1/public/class-wp-tripadvisor-review-slider-public.php	2025-12-05 19:08:06.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-tripadvisor-review-slider/14.2/public/class-wp-tripadvisor-review-slider-public.php	2026-02-09 19:59:22.000000000 +0000
@@ -131,14 +131,19 @@
 		
 		wp_enqueue_script( $this->_token."_plublic", plugin_dir_url( __FILE__ ) . 'js/wprev-public.js', array( 'jquery' ), $this->version, false );
 
-
-		wp_localize_script($this->_token."_plublic", 'wprevpublicjs_script_vars', 
-						array(
-						'wpfb_nonce'=> wp_create_nonce('randomnoncestring'),
-						'wpfb_ajaxurl' => admin_url( 'admin-ajax.php' ),
-						'wprevpluginsurl' => wprev_trip_plugin_url
-						)
-					);
+		// SECURITY FIX: Only expose nonce to administrators who need it
+		// Public users don't need access to admin AJAX endpoints
+		$script_vars = array(
+			'wpfb_ajaxurl' => admin_url( 'admin-ajax.php' ),
+			'wprevpluginsurl' => wprev_trip_plugin_url
+		);
+		
+		// Only add nonce for users with manage_options capability
+		if (current_user_can('manage_options')) {
+			$script_vars['wpfb_nonce'] = wp_create_nonce('randomnoncestring');
+		}
+		
+		wp_localize_script($this->_token."_plublic", 'wprevpublicjs_script_vars', $script_vars);

Exploit Outline

1. **Identify Target Nonce:** Log in as a Subscriber and visit any page where the TripAdvisor slider is active. Extract the `wpfb_nonce` from the `window.wprevpublicjs_script_vars` object in the page source. 2. **Craft Payload:** Prepare a malicious payload, such as a script breakout like `</style><script>alert(document.domain)</script>` for CSS fields or `<img src=x onerror=alert(1)>` for review fields. 3. **Execute AJAX Request:** Send a POST request to `/wp-admin/admin-ajax.php` using the extracted nonce. The action can be `wprev_tripadvisor_save_template` (to modify slider settings) or similar handlers that process the `postreviewarray` (to inject reviews). 4. **Trigger XSS:** The payload will be saved to the database. It will execute when an administrator views the Review List in the backend or when a visitor views a page containing the injected shortcode template.

Check if your site is affected.

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