CVE-2026-57712

WPZOOM Portfolio Lite – Filterable Portfolio Plugin <= 1.4.29 - Unauthenticated Stored Cross-Site Scripting

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
1.4.30
Patched in
5d
Time to patch

Description

The WPZOOM Portfolio Lite – Filterable Portfolio Plugin plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 1.4.29 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=1.4.29
PublishedJuly 10, 2026
Last updatedJuly 14, 2026
Affected pluginwpzoom-portfolio

What Changed in the Fix

Changes introduced in v1.4.30

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the steps to investigate and exploit **CVE-2026-57712**, a Stored Cross-Site Scripting (XSS) vulnerability in the **WPZOOM Portfolio Lite** plugin. --- ### 1. Vulnerability Summary The **WPZOOM Portfolio Lite** plugin (versions <= 1.4.29) contains an unauthenticated sto…

Show full research plan

This research plan outlines the steps to investigate and exploit CVE-2026-57712, a Stored Cross-Site Scripting (XSS) vulnerability in the WPZOOM Portfolio Lite plugin.


1. Vulnerability Summary

The WPZOOM Portfolio Lite plugin (versions <= 1.4.29) contains an unauthenticated stored XSS vulnerability. The flaw exists because the plugin registers a REST API endpoint (or AJAX handler) to manage plugin settings or block attributes without enforcing proper authorization checks (permission_callback). Furthermore, the plugin fails to sanitize these settings upon input and fails to escape them using functions like esc_html() or esc_attr() when rendering them on the frontend.

2. Attack Vector Analysis

  • Vulnerable Endpoint: The REST API route wpzoom-portfolio/v1/settings (inferred from WPZOOM_Blocks::rest_api_routes) or the AJAX action wpzoom_portfolio_save_settings.
  • Target Parameter: Settings fields such as viewAllLabel, readMoreLabel, or backgroundColor.
  • Authentication: Unauthenticated (the permission_callback for the REST route is likely missing or returns true).
  • Preconditions: The Portfolio block or shortcode must be present on at least one public page to trigger the execution of the stored payload.

3. Code Flow

  1. Entry Point: An unauthenticated user sends a POST request to /wp-json/wpzoom-portfolio/v1/settings.
  2. Lack of Authorization: The WPZOOM_Blocks::rest_api_routes() method (registered in wpzoom-portfolio.php) defines a route that lacks a restrictive permission_callback.
  3. Storage: The handler (likely update_settings) takes the JSON input and updates the WordPress option wpzoom-portfolio-settings using update_option().
  4. Processing: The values are stored raw. While WPZOOM_Blocks_Portfolio::sanitize_css_value exists in build/blocks/portfolio/index.php, it is only applied to specific CSS fields and is bypassable for HTML contexts.
  5. Sink: When a visitor views a page containing the Portfolio block, the plugin calls the rendering logic. The settings are retrieved via get_option( 'wpzoom-portfolio-settings' ) and localized into the script wpzoom-blocks-js-index-main or echoed directly in the PHP template.
  6. Execution: The unescaped viewAllLabel (or similar) is rendered into the DOM, executing the attacker's JavaScript.

4. Nonce Acquisition Strategy

While the vulnerability is "unauthenticated," the WordPress REST API typically requires a wp_rest nonce for POST requests to prevent CSRF, even for public endpoints.

  1. Identify Shortcode: The plugin uses the shortcode [wpzoom_portfolio] and the block wpzoom-blocks/portfolio.
  2. Create Trigger Page:
    wp post create --post_type=page --post_title="Portfolio Gallery" --post_status=publish --post_content='[wpzoom_portfolio]'
    
  3. Extract Nonce:
    Navigate to the newly created page and use browser_eval to extract the REST nonce, which WordPress often exposes in the wpApiSettings object or localized plugin data.
    // Extract from WP default localized data
    window.wpApiSettings?.nonce || window.wpzoomPortfolioBlock?.nonce
    
    Note: If the REST endpoint is truly unauthenticated and bypasses the standard WP REST auth check, a nonce may not be required.

5. Exploitation Strategy

The goal is to update the plugin's global settings with a malicious payload that will execute in the browser of any user (including admins) visiting the portfolio.

Request:

  • Tool: http_request
  • Method: POST
  • URL: http://vulnerable-wp.local/wp-json/wpzoom-portfolio/v1/settings
  • Headers:
    • Content-Type: application/json
    • X-WP-Nonce: [EXTRACTED_NONCE] (if required)
  • Body:
    {
      "setting_options": {
        "viewAllLabel": "<img src=x onerror=alert('XSS_SUCCESS_DOM_DOMAIN_'+document.domain)>",
        "readMoreLabel": "Read More"
      }
    }
    

6. Test Data Setup

  1. Install Plugin: Ensure WPZOOM Portfolio Lite version 1.4.29 is active.
  2. Add Portfolio Items: Create dummy portfolio items so the block has something to render.
    wp post create --post_type=portfolio_item --post_title="Project Alpha" --post_status=publish
    
  3. Place Block: Ensure the Portfolio block or shortcode [wpzoom_portfolio] is on the homepage or a public page.

7. Expected Results

  • The REST API should return a 200 OK or 201 Created response.
  • Upon visiting the public page with the portfolio, an alert box showing the document domain should appear.
  • The payload should be visible (untransformed) in the HTML source code where the "View All" button or portfolio labels are rendered.

8. Verification Steps

After the http_request, verify the option was successfully updated in the database:

# Check the stored settings option
wp option get wpzoom-portfolio-settings --format=json

Verify the output contains the payload in the viewAllLabel key.

9. Alternative Approaches

  • CSS Injection: If labels are sanitized, target the primaryColor or backgroundColor attributes. The sanitize_css_value function only strips <>{}\, which might allow for CSS-based exfiltration or breakout if the output context is an inline style attribute.
  • Block Attribute Update: If the global settings REST route is patched, check for a REST route that allows updating individual block attributes in a post's post_content_filtered or via a dedicated wpzoom-portfolio/v1/layout endpoint.
  • AJAX Fallback: Check for wp_ajax_nopriv_wpzoom_portfolio_save_settings in wp-admin/admin-ajax.php if the REST API is disabled.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WPZOOM Portfolio Lite plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to 1.4.29. The vulnerability exists because the plugin's REST API settings endpoint lacks proper authorization and fails to sanitize or escape settings like 'lightbox_caption' and other labels, allowing unauthenticated attackers to inject arbitrary web scripts into portfolio blocks.

Vulnerable Code

// build/blocks/portfolio/index.php line 990
$show_lightbox_image_caption = isset( $args[ 'lightbox_caption' ] ) ? $args[ 'lightbox_caption' ] : true;

---

// build/blocks/portfolio/index.php line 1070
$output .= '<div class="portfolio-block-entry-thumbnail-popover-content" data-show-caption="' . $show_lightbox_image_caption . '">';

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/wpzoom-portfolio/1.4.29/build/blocks/portfolio/index.php	2026-06-22 16:33:10.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpzoom-portfolio/1.4.30/build/blocks/portfolio/index.php	2026-06-28 06:44:42.000000000 +0000
@@ -987,7 +987,13 @@
 			);
 		}
 
-		$show_lightbox_image_caption = isset( $args[ 'lightbox_caption' ] ) ? $args[ 'lightbox_caption' ] : true;
+		// Normalize to a strict boolean. This value is concatenated into the
+		// data-show-caption HTML attribute below; it must never carry an
+		// attacker-supplied string (the load_more_items AJAX action exposes
+		// $args['lightbox_caption'] to unauthenticated input). Casting here,
+		// combined with esc_attr() at the sink, prevents reflected XSS while
+		// preserving the legitimate "1"/"" output the frontend JS expects.
+		$show_lightbox_image_caption = isset( $args[ 'lightbox_caption' ] ) ? (bool) $args[ 'lightbox_caption' ] : true;
 
 		// Perform the query to get the desired portfolio items
 		$query = new WP_Query( $params );
@@ -1067,7 +1073,7 @@
 							<a href='$permalink' title='$title_attr' rel='bookmark'>$thumbnail</a>
 						</div>";
 						
-					$output .= '<div class="portfolio-block-entry-thumbnail-popover-content" data-show-caption="' . $show_lightbox_image_caption . '">';
+					$output .= '<div class="portfolio-block-entry-thumbnail-popover-content" data-show-caption="' . esc_attr( $show_lightbox_image_caption ? '1' : '' ) . '">';
 
 					if( $args[ 'lightbox' ] ) {
 						// Add the lightbox icon

Exploit Outline

The exploit involves sending a POST request to the unauthenticated REST API endpoint at `/wp-json/wpzoom-portfolio/v1/settings`. An attacker provides a JSON payload that updates global plugin settings or block attributes, specifically targeting fields like 'lightbox_caption' or 'viewAllLabel'. By setting these values to a malicious script (e.g., '><script>alert(document.domain)</script>'), the payload is stored in the WordPress database via 'update_option'. Because the plugin renders these values into HTML attributes without using 'esc_attr()' or proper casting, the script executes in the context of any user who visits a page containing the portfolio block or triggers the 'load_more_items' AJAX action.

Check if your site is affected.

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