CVE-2026-32491

WP Review Slider <= 13.9 - 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.0
Patched in
4d
Time to patch

Description

The WP Review Slider plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 13.9 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<=13.9
PublishedMarch 23, 2026
Last updatedMarch 26, 2026
Affected pluginwp-facebook-reviews

What Changed in the Fix

Changes introduced in v14.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

## Vulnerability Summary The **WP Review Slider** plugin (versions <= 13.9) is vulnerable to **Authenticated Stored Cross-Site Scripting (XSS)**. The vulnerability exists because the plugin's AJAX handler for manually adding or saving reviews (likely `wprev_save_manual_review`) fails to perform adeq…

Show full research plan

Vulnerability Summary

The WP Review Slider plugin (versions <= 13.9) is vulnerable to Authenticated Stored Cross-Site Scripting (XSS). The vulnerability exists because the plugin's AJAX handler for manually adding or saving reviews (likely wprev_save_manual_review) fails to perform adequate capability checks (missing current_user_can('manage_options')) and does not sanitize or escape review data before storing it in the database and subsequently rendering it on the admin Review List and public slider pages.

This allows an authenticated attacker with Subscriber-level access to inject malicious JavaScript into the reviewer_name or review_text fields of a review. When an administrator views the reviews in the dashboard or a visitor views a page containing the review slider, the script executes in their browser context.

Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: wprev_save_manual_review (inferred from plugin lineage)
  • Vulnerable Parameters: reviewer_name, review_text
  • Authentication Level: Subscriber or higher (Authenticated)
  • Nonce Action: randomnoncestring
  • Nonce Source: Exposed via wp_localize_script in both admin and public contexts under the variable wprevpublicjs_script_vars or adminjs_script_vars.

Code Flow

  1. Registration: The plugin registers an AJAX action wp_ajax_wprev_save_manual_review in the admin initialization logic.
  2. Access: The handler likely calls check_ajax_referer('randomnoncestring', 'wpfb_nonce') but lacks a current_user_can() check, allowing any logged-in user to reach the code.
  3. Storage: User-provided data from $_POST['reviewer_name'] and $_POST['review_text'] is inserted into the {wpdb->prefix}wpfb_reviews table using $wpdb->insert or $wpdb->update.
  4. Rendering (Admin Sink): In admin/partials/review_list.php, the plugin retrieves rows from the database and echoes the reviewer_name without escaping (e.g., echo $row->reviewer_name).
  5. Rendering (Public Sink): When the [wprevpro_usetemplate] shortcode is used, public/partials/wp-fb-reviews-public-display.php renders the reviews, potentially echoing the malicious fields unsafely.

Nonce Acquisition Strategy

The nonce for the action randomnoncestring is enqueued on any page where the plugin's public scripts are loaded.

  1. Create Trigger Page: Create a public post or page containing the plugin's shortcode to ensure scripts are enqueued.
    wp post create --post_type=page --post_title="Review Test" --post_status=publish --post_content='[wprevpro_usetemplate tid="1"]'
    
  2. Navigate as Subscriber: Use browser_navigate to visit the newly created page while logged in as a Subscriber.
  3. Extract Nonce: Use browser_eval to extract the nonce from the localized JavaScript object.
    • Variable Name: wprevpublicjs_script_vars (from public/class-wp-fb-reviews-public.php)
    • Key: wpfb_nonce
    • JS Path: window.wprevpublicjs_script_vars?.wpfb_nonce

Exploitation Strategy

  1. Target User: Authenticate as a Subscriber.
  2. Obtain Nonce: Navigate to the page with the shortcode and extract the nonce using the strategy above.
  3. Send Exploit Request: Use http_request to send a POST request to admin-ajax.php.
    • URL: http://<target>/wp-admin/admin-ajax.php
    • Method: POST
    • Headers: Content-Type: application/x-www-form-urlencoded
    • Body Parameters:
      • action: wprev_save_manual_review
      • wpfb_nonce: <EXTRACTED_NONCE>
      • reviewer_name: Attacker <img src=x onerror=alert('XSS_SUCCESS')>
      • review_text: This is a malicious review.
      • rating: 5
      • type: Facebook (to ensure it appears in the default list)

Test Data Setup

  1. Plugin Configuration: Ensure the plugin is active and the database table wpfb_reviews is created (automatically
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP Review Slider plugin for WordPress is vulnerable to Authenticated Stored Cross-Site Scripting via the 'wprev_save_manual_review' AJAX action due to a lack of capability checks and insufficient output escaping. This allows authenticated attackers with subscriber-level permissions to inject malicious scripts into review fields, which execute when an administrator views the review list or a visitor views a page containing the review slider.

Vulnerable Code

// admin/class-wp-fb-reviews-admin.php
// In version 13.9, AJAX handlers like wprev_save_manual_review lacked capability checks and output escaping

		check_ajax_referer('randomnoncestring', 'wpfb_nonce');
		
		$postreviewarray = $_POST['postreviewarray'];

--- 

// admin/class-wp-fb-reviews-admin.php around line 1475
			$avatarhtml = '';
			if(isset($review->userpic) && $review->userpic!=''){
				$avatarhtml = '<img alt="" src="'.$review->userpic.'" class="wprev_dash_avatar" height="40" width="40">';
			}
			
			echo '<li><div class="wprev_dash_revdiv">'.$avatarhtml.'<div class="wprev_dash_stars">'.$starhtml.'</div><h4 class="wprev_dash_name">'.$review->reviewer_name.' - <span class="wprev_dash_timeago">'.$daysagohtml.'</span></h4><p class="wprev_dash_text">'.$reviewtext.'</p></div></li>';

--- 

// admin/partials/review_list.php around line 265
				$html .= '<tr id="'.$reviewsrow->id.'"  class="'.$hiddentrclass.'">
						<th scope="col" class="manage-column">
						<a title="hide/unhide" alt="hide/unhide" href="'.$hideurl.'">'.$hideicon.'</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">'.$reviewsrow->recommendation_type.'</th>
						<th scope="col" class="manage-column">'.$reviewsrow->review_text.'</th>
						<th scope="col" class="manage-column">'.$reviewsrow->created_time.'</th>
						<th scope="col" class="manage-column">'.$reviewsrow->review_length.'</th>
						<th scope="col" class="manage-column">'.$reviewsrow->pagename.'</th>
						<th scope="col" class="manage-column">'.$reviewsrow->type.'</th>
					</tr>';

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-facebook-reviews/13.9/admin/class-wp-fb-reviews-admin.php /home/deploy/wp-safety.org/data/plugin-versions/wp-facebook-reviews/14.0/admin/class-wp-fb-reviews-admin.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-facebook-reviews/13.9/admin/class-wp-fb-reviews-admin.php	2025-02-11 23:19:10.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-facebook-reviews/14.0/admin/class-wp-fb-reviews-admin.php	2026-02-09 17:40:36.000000000 +0000
@@ -579,6 +579,12 @@
 		
 		check_ajax_referer('randomnoncestring', 'wpfb_nonce');
 		
+		// Security: Only allow administrators to insert reviews
+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_send_json_error( 'Unauthorized access', 403 );
+			wp_die();
+		}
+		
 		$postreviewarray = $_POST['postreviewarray'];
 		
 		//var_dump($postreviewarray);
@@ -1471,12 +1480,12 @@
 			$starfile = 'stars_'.$review->rating.'_yellow.png';
 			$starhtml='<img src="'.$imgs_url."" . $starfile.'" alt="'.$review->rating.' star rating" class="wprev_dash_stars">';
 			
-			$avatarhtml = '';
-			if(isset($review->userpic) && $review->userpic!=''){
-				$avatarhtml = '<img alt="" src="'.$review->userpic.'" class="wprev_dash_avatar" height="40" width="40">';
-			}
-			
-			echo '<li><div class="wprev_dash_revdiv">'.$avatarhtml.'<div class="wprev_dash_stars">'.$starhtml.'</div><h4 class="wprev_dash_name">'.$review->reviewer_name.' - <span class="wprev_dash_timeago">'.$daysagohtml.'</span></h4><p class="wprev_dash_text">'.$reviewtext.'</p></div></li>';
+		$avatarhtml = '';
+		if(isset($review->userpic) && $review->userpic!=''){
+			$avatarhtml = '<img alt="" src="'.esc_url($review->userpic).'" class="wprev_dash_avatar" height="40" width="40">';
+		}
+		
+		echo '<li><div class="wprev_dash_revdiv">'.$avatarhtml.'<div class="wprev_dash_stars">'.$starhtml.'</div><h4 class="wprev_dash_name">'.esc_html($review->reviewer_name).' - <span class="wprev_dash_timeago">'.$daysagohtml.'</span></h4><p class="wprev_dash_text">'.esc_html($reviewtext).'</p></div></li>';
 			
 		}
 		echo '</ul>';
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-facebook-reviews/13.9/admin/partials/review_list.php /home/deploy/wp-safety.org/data/plugin-versions/wp-facebook-reviews/14.0/admin/partials/review_list.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-facebook-reviews/13.9/admin/partials/review_list.php	2025-02-11 23:19:10.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-facebook-reviews/14.0/admin/partials/review_list.php	2026-02-09 17:40:36.000000000 +0000
@@ -262,20 +262,20 @@
 					$userpic = '<a href="'.$profilelink.'" target=_blank>'.$userpic.'</a>';
 				}
 	
-				$html .= '<tr id="'.$reviewsrow->id.'"  class="'.$hiddentrclass.'">
-						<th scope="col" class="manage-column">
-						<a title="hide/unhide" alt="hide/unhide" href="'.$hideurl.'">'.$hideicon.'</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">'.$reviewsrow->recommendation_type.'</th>
-						<th scope="col" class="manage-column">'.$reviewsrow->review_text.'</th>
-						<th scope="col" class="manage-column">'.$reviewsrow->created_time.'</th>
-						<th scope="col" class="manage-column">'.$reviewsrow->review_length.'</th>
-						<th scope="col" class="manage-column">'.$reviewsrow->pagename.'</th>
-						<th scope="col" class="manage-column">'.$reviewsrow->type.'</th>
-					</tr>';
+			$html .= '<tr id="'.$reviewsrow->id.'"  class="'.$hiddentrclass.'">
+					<th scope="col" class="manage-column">
+					<a title="hide/unhide" alt="hide/unhide" href="'.$hideurl.'">'.$hideicon.'</a>
+					</th>
+					<th scope="col" class="manage-column">'.$userpic.'</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">'.esc_html($reviewsrow->recommendation_type).'</th>
+					<th scope="col" class="manage-column">'.esc_html($reviewsrow->review_text).'</th>
+					<th scope="col" class="manage-column">'.esc_html($reviewsrow->created_time).'</th>
+					<th scope="col" class="manage-column">'.esc_html($reviewsrow->review_length).'</th>
+					<th scope="col" class="manage-column">'.esc_html($reviewsrow->pagename).'</th>
+					<th scope="col" class="manage-column">'.esc_html($reviewsrow->type).'</th>
+				</tr>';

Exploit Outline

The exploit target is the `wprev_save_manual_review` AJAX action. An authenticated attacker with Subscriber-level access can obtain the required nonce (`randomnoncestring`) from the frontend, where it is localized as `wprevpublicjs_script_vars.wpfb_nonce`. Using this nonce, the attacker sends a POST request to `/wp-admin/admin-ajax.php` containing a malicious JavaScript payload in the `reviewer_name` or `review_text` parameters. Because the plugin does not verify if the user has administrative privileges and fails to escape the stored values upon output in the admin dashboard or public pages, the script executes in the context of any user who views the reviews.

Check if your site is affected.

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