CVE-2026-39597

WPZOOM Addons for Elementor – Starter Templates & Widgets <= 1.3.4 - Reflected Cross-Site Scripting

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.1
CVSS Score
6.1
CVSS Score
medium
Severity
1.3.5
Patched in
6d
Time to patch

Description

The WPZOOM Addons for Elementor – Starter Templates & Widgets plugin for WordPress is vulnerable to Reflected Cross-Site Scripting in versions up to, and including, 1.3.4 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that execute if they can successfully trick a user into performing an action such as clicking on a link.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
Required
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.3.4
PublishedApril 16, 2026
Last updatedApril 21, 2026

What Changed in the Fix

Changes introduced in v1.3.5

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Research Plan: CVE-2026-39597 - WPZOOM Addons for Elementor Reflected XSS ## 1. Vulnerability Summary The **WPZOOM Addons for Elementor – Starter Templates & Widgets** plugin (versions <= 1.3.4) is vulnerable to Reflected Cross-Site Scripting (XSS) via the `wpz_posts_grid_load_more` AJAX action. …

Show full research plan

Research Plan: CVE-2026-39597 - WPZOOM Addons for Elementor Reflected XSS

1. Vulnerability Summary

The WPZOOM Addons for Elementor – Starter Templates & Widgets plugin (versions <= 1.3.4) is vulnerable to Reflected Cross-Site Scripting (XSS) via the wpz_posts_grid_load_more AJAX action. The vulnerability exists in the WPZOOM_Elementor_Ajax_Post_Grid class where the title_tag parameter, provided within a JSON-encoded posts_data POST parameter, is reflected into the page without sufficient sanitization or attribute escaping. Specifically, the value is used as an HTML tag name, allowing an attacker to inject attributes like onerror.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: wpz_posts_grid_load_more
  • Vulnerable Parameter: posts_data[title_tag]
  • Authentication: Unauthenticated (available via wp_ajax_nopriv_wpz_posts_grid_load_more).
  • Preconditions:
    1. A valid WordPress nonce for the action wpz_posts_grid_load_more.
    2. At least one published post must exist so that the WP_Query returns results, triggering the rendering logic.
  • Payload Type: Reflected (via POST request).

3. Code Flow

  1. Entry Point: The ajax_post_grid_load_more() function in includes/wpzoom-elementor-ajax-posts-grid.php is triggered by the AJAX action.
  2. Input Handling:
    • The nonce is verified (Line 72).
    • $_POST['posts_data'] is passed through sanitize_text_field() and then json_decode() (Lines 74-75).
    • The resulting array is stored in self::$settings (Line 77).
  3. Execution Path:
    • A WP_Query is executed based on other parameters in $data
Research Findings
Static analysis — not yet PoC-verified

Summary

The plugin is vulnerable to unauthenticated Reflected Cross-Site Scripting (XSS) via the 'wpz_posts_grid_load_more' AJAX action. This occurs because the 'title_tag' parameter within the JSON-encoded 'posts_data' POST parameter is reflected into the page as an HTML tag name without validation or escaping.

Vulnerable Code

// includes/wpzoom-elementor-ajax-posts-grid.php line 204
	protected function render_title() {	
		$settings = $this->get_settings();

		$show_title = $settings[ 'show_title' ];

		if ( 'yes' !== $show_title ) {
			return;
		}

		$title_tag = $settings[ 'title_tag' ];
			
		?>
		<<?php echo $title_tag; // WPCS: XSS OK. ?> class="title">
			<a href="<?php the_permalink(); ?>" title="<?php echo esc_attr( get_the_title() ); ?>"><?php the_title(); ?></a>
		</<?php echo $title_tag; // WPCS: XSS OK. ?>>
		<?php
	}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpzoom-elementor-addons/1.3.4/includes/widgets/portfolio-reel/portfolio-reel.php /home/deploy/wp-safety.org/data/plugin-versions/wpzoom-elementor-addons/1.3.5/includes/widgets/portfolio-reel/portfolio-reel.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpzoom-elementor-addons/1.3.4/includes/widgets/portfolio-reel/portfolio-reel.php	2026-02-13 13:48:16.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpzoom-elementor-addons/1.3.5/includes/widgets/portfolio-reel/portfolio-reel.php	2026-02-23 15:59:50.000000000 +0000
@@ -1743,7 +1743,7 @@
 
                 <?php if ( $popup_video_type === 'self_hosted' && $is_video_popup_self_hosted ): ?>
                     <div id="zoom-popup-<?php echo the_ID(); ?>" class="animated slow mfp-hide"
-                         data-src="<?php echo $popup_self_hosted_src; ?>">
+                         data-src="<?php echo esc_url( $popup_self_hosted_src ); ?>">
                         <div class="mfp-iframe-scaler">
                             <?php
                             echo wp_video_shortcode(
@@ -1771,12 +1771,12 @@
                 <a class="reel_video_item" href="<?php echo esc_url( get_permalink() ); ?>" title="<?php echo esc_attr( get_the_title() ); ?>">
                     <?php if($has_video_popup): ?>
                     <div class="entry-thumbnail-popover<?php if ($lightbox_open_thumb) { ?> lightbox_open_full<?php } ?>">
-                        <div class="entry-thumbnail-popover-content popover-content--animated" data-show-caption="<?php echo $show_popup_caption ?>">
+                        <div class="entry-thumbnail-popover-content popover-content--animated" data-show-caption="<?php echo esc_attr( $show_popup_caption ); ?>">
                             <?php if ( $popup_video_type === 'self_hosted' && $is_video_popup_self_hosted ): ?>
                                 <span href="#zoom-popup-<?php echo the_ID(); ?>" class="mfp-inline portfolio-popup-video"></span>
                             <?php elseif ( $popup_video_type === 'external_hosted' && ! empty( $portfolio_video_popup_url ) ): ?>
                                 <span class="mfp-iframe portfolio-popup-video"
-                                      href="<?php echo $portfolio_video_popup_url; ?>"></span>
+                                      href="<?php echo esc_url( $portfolio_video_popup_url ); ?>"></span>
                             <?php endif; ?>
                         </div>
                     </div>
@@ -1833,11 +1833,11 @@
 
                                 <ul>
                                     <?php if ($enable_director_name && $video_director) { ?>
-                                       <li><?php echo $video_director; ?></li>
+                                       <li><?php echo esc_html( $video_director ); ?></li>
                                     <?php } ?>
 
                                     <?php if ($enable_year && $video_year) { ?>
-                                       <li><?php echo $video_year; ?></li>
+                                       <li><?php echo esc_html( $video_year ); ?></li>
                                     <?php } ?>
 
                                     <?php if ( $enable_category ) : ?><li>
@@ -1869,11 +1869,11 @@
 
                                 <ul>
                                     <?php if ($enable_director_name && $video_director) { ?>
-                                       <li><?php echo $video_director; ?></li>
+                                       <li><?php echo esc_html( $video_director ); ?></li>
                                     <?php } ?>
 
                                     <?php if ($enable_year && $video_year) { ?>
-                                       <li><?php echo $video_year; ?></li>
+                                       <li><?php echo esc_html( $video_year ); ?></li>
                                     <?php } ?>
 
                                     <?php if ( $enable_category ) : ?><li>
@@ -1907,11 +1907,11 @@
 
                             <ul>
                                 <?php if ($enable_director_name && $video_director) { ?>
-                                   <li><?php echo $video_director; ?></li>
+                                   <li><?php echo esc_html( $video_director ); ?></li>
                                 <?php } ?>
 
                                 <?php if ($enable_year && $video_year) { ?>
-                                   <li><?php echo $video_year; ?></li>
+                                   <li><?php echo esc_html( $video_year ); ?></li>
                                 <?php } ?>
 
                                 <?php if ( $enable_category ) : ?><li>
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpzoom-elementor-addons/1.3.4/includes/widgets/slider-pro/slider-pro.php /home/deploy/wp-safety.org/data/plugin-versions/wpzoom-elementor-addons/1.3.5/includes/widgets/slider-pro/slider-pro.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpzoom-elementor-addons/1.3.4/includes/widgets/slider-pro/slider-pro.php	2025-06-12 19:59:44.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpzoom-elementor-addons/1.3.5/includes/widgets/slider-pro/slider-pro.php	2026-02-23 15:59:50.000000000 +0000
@@ -1131,7 +1131,7 @@
                                 <a href="#zoom-popup-<?php echo get_the_ID(); ?>" data-popup-video-options='<?php echo json_encode( $encode_lightbox_video_opts ); ?>' data-popup-type="inline" class="popup-video" aria-label="Watch Video"></a>
 
                                 <?php elseif(!empty($video_background_popup_url)): ?>
-                                    <a data-popup-type="iframe" data-popup-video-options='<?php echo json_encode( $encode_lightbox_video_opts ); ?>' class="popup-video animated slow pulse" href="<?php echo $video_background_popup_url ?>" aria-label="Watch Video"></a>
+                                    <a data-popup-type="iframe" data-popup-video-options='<?php echo json_encode( $encode_lightbox_video_opts ); ?>' class="popup-video animated slow pulse" href="<?php echo esc_url( $video_background_popup_url ); ?>" aria-label="Watch Video"></a>
                                 <?php endif; ?>
 
                             <?php } /* End Inspiro PRO markup */ ?>
@@ -1174,7 +1174,7 @@
                                      if( ( 'inspiro' === $current_theme && class_exists( 'WPZOOM' ) && $align_vertical != 'bottom' ) || ( 'wpzoom-inspiro-pro' === $current_theme && $align == 'center' ) ) {
 								?>
                                 <?php if($popup_video_type === 'self_hosted' && $is_video_popup): ?>
-                                    <div id="zoom-popup-<?php echo get_the_ID(); ?>"  class="animated slow mfp-hide" data-src ="<?php echo $popup_final_external_src ?>">
+                                    <div id="zoom-popup-<?php echo get_the_ID(); ?>"  class="animated slow mfp-hide" data-src ="<?php echo esc_url( $popup_final_external_src ); ?>">
 
                                         <div class="mfp-iframe-scaler">
 
@@ -1192,7 +1192,7 @@
                                     <a href="#zoom-popup-<?php echo get_the_ID(); ?>" data-popup-video-options='<?php echo json_encode( $encode_lightbox_video_opts ); ?>' data-popup-type="inline" class="popup-video" aria-label="Watch Video"></a>
 
                                 <?php elseif(!empty($video_background_popup_url)): ?>
-                                    <a data-popup-type="iframe" data-popup-video-options='<?php echo json_encode( $encode_lightbox_video_opts ); ?>' class="popup-video animated slow pulse" href="<?php echo $video_background_popup_url ?>" aria-label="Watch Video"></a>
+                                    <a data-popup-type="iframe" data-popup-video-options='<?php echo json_encode( $encode_lightbox_video_opts ); ?>' class="popup-video animated slow pulse" href="<?php echo esc_url( $video_background_popup_url ); ?>" aria-label="Watch Video"></a>
                                 <?php endif; ?>
 
                                 <?php } /* End Inspiro Premium markup */ ?>
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpzoom-elementor-addons/1.3.4/includes/wpzoom-elementor-ajax-posts-grid.php /home/deploy/wp-safety.org/data/plugin-versions/wpzoom-elementor-addons/1.3.5/includes/wpzoom-elementor-ajax-posts-grid.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpzoom-elementor-addons/1.3.4/includes/wpzoom-elementor-ajax-posts-grid.php	2026-02-10 20:54:40.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpzoom-elementor-addons/1.3.5/includes/wpzoom-elementor-ajax-posts-grid.php	2026-02-23 15:59:50.000000000 +0000
@@ -210,8 +210,8 @@
 			return;
 		}
 
-		$title_tag = $settings[ 'title_tag' ];
-			
+		$title_tag = \Elementor\Utils::validate_html_tag( $settings[ 'title_tag' ] );
+
 		?>
 		<<?php echo $title_tag; // WPCS: XSS OK. ?> class="title">
 			<a href="<?php the_permalink(); ?>" title="<?php echo esc_attr( get_the_title() ); ?>"><?php the_title(); ?></a>

Exploit Outline

The exploit targets the 'wpz_posts_grid_load_more' AJAX action via the /wp-admin/admin-ajax.php endpoint. An attacker sends a POST request with the 'action' set to 'wpz_posts_grid_load_more', a valid 'nonce', and a 'posts_data' parameter containing a JSON-encoded object. By setting the 'title_tag' key within this JSON object to a malicious payload (e.g., 'img src=x onerror=alert(1)'), the plugin renders the string directly as an HTML tag name when iterating through posts. Because the action is hooked to 'wp_ajax_nopriv_', it is accessible to unauthenticated attackers, provided they can obtain a valid nonce through social engineering or other means.

Check if your site is affected.

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