WP TripAdvisor Review Slider <= 14.1 - Authenticated (Subscriber+) Stored Cross-Site Scripting
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:NTechnical Details
<=14.1What Changed in the Fix
Changes introduced in v14.2
Source Code
WordPress.org SVN### 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 tablewptripadvisor_post_templates). - Vulnerable Parameter:
template_css(stored in thewptripadvisor_post_templatestable). - Authentication: Authenticated (Subscriber or higher).
- Nonce: Required. The nonce action is
randomnoncestring. - Preconditions:
- At least one template must exist in the database (usually created by an admin during initial setup).
- The shortcode
[wptripadvisor_usetemplate]must be present on a public page to leak the required nonce.
3. Code Flow
- Input: A Subscriber sends a POST request to
admin-ajax.phpwith the actionwprev_tripadvisor_save_template. - Processing: The AJAX handler (in
WP_TripAdvisor_Review_Admin) verifies the noncerandomnoncestring. Crucially, it fails to perform a capability check (e.g.,current_user_can('manage_options')). - Storage: The handler takes the input (likely from a
form_datastring or direct POST params) and updates thewptripadvisor_post_templatestable.- Reference:
includes/class-wp-tripadvisor-review-slider.phpdefines the table schema, includingtemplate_css text NOT NULL.
- Reference:
- Public Output: A user visits a page containing the shortcode
[wptripadvisor_usetemplate tid="1"]. - Rendering:
public/class-wp-tripadvisor-review-slider-public.phpcallswptripadvisor_usetemplate_func, which includespublic/partials/wp-tripadvisor-review-slider-public-display.php. - Sink: The display logic fetches the
template_cssfrom the database and echoes it inside a<style>tag or directly into the page without usingwp_strip_all_tagsoresc_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.
- Identify Shortcode: The shortcode is
[wptripadvisor_usetemplate tid="1"]. - Navigation: Navigate to a page where this shortcode is present.
- Variable Extraction: The plugin uses
wp_localize_scriptinpublic/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).
- JS Object:
- 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
- Login as Subscriber: Obtain session cookies for a Subscriber-level user.
- Get Nonce: Navigate to a public page containing the TripAdvisor slider and extract the
wpfb_nonce. - Craft Payload: The goal is to inject a script into the template settings. Since
template_cssis often placed inside a<style>block, we will break out of it.- Payload:
</style><script>alert(document.domain)</script>
- Payload:
- 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:
(Note: If the plugin expectsaction=wprev_tripadvisor_save_template& nonce=[EXTRACTED_NONCE]& template_id=1& template_css=</style><script>alert(document.domain)</script>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
- Administrator: Create a template via the plugin menu (WP TA Reviews > Templates). This creates ID 1 in
wptripadvisor_post_templates. - Administrator: Create a public Post or Page and insert the shortcode:
[wptripadvisor_usetemplate tid="1"]. - Administrator: Create a user with the Subscriber role.
7. Expected Results
- The AJAX request should return a successful response (likely a JSON
1or 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
- Database Check: Use WP-CLI to verify the stored payload.
wp db query "SELECT template_css FROM wp_wptripadvisor_post_templates WHERE id=1;" - 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)>
- Payload:
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
@@ -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); @@ -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>'; } @@ -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.