CVE-2026-49069

WPZOOM Portfolio Lite – Filterable Portfolio Plugin <= 1.4.21 - 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.22
Patched in
8d
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.21 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.21
PublishedJune 8, 2026
Last updatedJune 15, 2026
Affected pluginwpzoom-portfolio

What Changed in the Fix

Changes introduced in v1.4.22

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This exploitation research plan targets **CVE-2026-49069**, an unauthenticated stored cross-site scripting (XSS) vulnerability in the **WPZOOM Portfolio Lite** plugin. ### 1. Vulnerability Summary The vulnerability exists because the plugin registers a REST API endpoint (or an AJAX handler) intende…

Show full research plan

This exploitation research plan targets CVE-2026-49069, an unauthenticated stored cross-site scripting (XSS) vulnerability in the WPZOOM Portfolio Lite plugin.

1. Vulnerability Summary

The vulnerability exists because the plugin registers a REST API endpoint (or an AJAX handler) intended for administrative configuration but fails to implement proper capability checks (permission_callback) and does not sanitize or escape the resulting settings before they are stored in the database or rendered in the application. An unauthenticated attacker can overwrite global plugin settings, injecting malicious scripts that will execute when an administrator visits the settings page or when a user views a portfolio block.

2. Attack Vector Analysis

  • Endpoint: POST /wp-json/wpzoom-portfolio/v1/settings (inferred from plugin slug and REST initialization).
  • Alternative Endpoint: POST /wp-admin/admin-ajax.php via an unauthenticated AJAX handler (less likely for "Stored" settings, but possible if the plugin uses a custom save routine).
  • Vulnerable Parameter: The JSON body of the REST request, specifically keys within the setting_options object such as readMoreLabel, viewAllLabel, or primaryColor.
  • Authentication: None (Unauthenticated).
  • Preconditions: The REST API must be enabled (default in WordPress).

3. Code Flow

  1. Registration: In wpzoom-portfolio.php (or build/blocks/portfolio/index.php), the WPZOOM_Blocks or WPZOOM_Blocks_Portfolio class hooks into rest_api_init.
  2. Route Definition: The rest_api_routes() method calls register_rest_route(). The vulnerability occurs if the permission_callback is either missing or set to __return_true.
  3. Data Sink (Storage): The callback function associated with the route receives the WP_REST_Request and calls update_option( 'wpzoom-portfolio-settings', $params ) without sanitizing the input.
  4. Execution (Output):
    • In wpzoom-portfolio.php, the enqueue_portfolio_block_editor_assets() method retrieves these options via get_option( 'wpzoom-portfolio-settings' ).
    • The values are localized into the JavaScript variable wpzoomPortfolioBlock via wp_localize_script().
    • Malicious scripts in readMoreLabel or viewAllLabel are rendered either in the Gutenberg editor's UI or on the frontend portfolio grid.

4. Nonce Acquisition Strategy

While unauthenticated REST endpoints often bypass nonces, WordPress may still expect a wp_rest nonce if certain security configurations are present.

  1. Identify Trigger: The portfolio scripts are enqueued on pages containing the portfolio block or layout.
  2. Create Test Page:
    wp post create --post_type=page --post_title="Portfolio Test" --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 from the WordPress core variable (often available to unauthenticated users if REST-enabled scripts are loaded):
    • browser_eval("wpApiSettings.nonce")
    • Alternatively, check the wpzoomPortfolioBlock object: browser_eval("window.wpzoomPortfolioBlock?.nonce") (if the developer added a custom nonce here).

5. Exploitation Strategy

Goal: Inject a stored XSS payload into the global plugin settings.

Step 1: Discover the exact REST endpoint
If /wp-json/wpzoom-portfolio/v1/settings returns a 404, check the REST index:
GET /wp-json/

Step 2: Send the Malicious Update Request

  • Tool: http_request
  • Method: POST
  • URL: http://<target>/wp-json/wpzoom-portfolio/v1/settings
  • Headers: Content-Type: application/json
  • Body:
    {
      "setting_options": {
        "readMoreLabel": "Read More<img src=x onerror=alert(document.domain)>",
        "viewAllLabel": "View All<script>alert('XSS')</script>"
      }
    }
    

Step 3: Trigger the XSS
Visit any page where a Portfolio block is rendered, or simply log in as an administrator and go to Portfolio > Settings. The payload stored in the wpzoom-portfolio-settings option will be localized into the page and executed by the browser.

6. Test Data Setup

  1. Install and activate wpzoom-portfolio version 1.4.21.
  2. Create a public portfolio item:
    wp post create --post_type=portfolio_item --post_title="Target Project" --post_status=publish
    
  3. Create a page to display the portfolio:
    wp post create --post_type=page --post_title="Gallery" --post_status=publish --post_content='<!-- wp:wpzoom-blocks/portfolio /-->'
    

7. Expected Results

  • The REST API call should return a 200 OK or 201 Created status code, indicating the settings were updated.
  • Upon visiting the "Gallery" page or the plugin settings in the dashboard, a JavaScript alert box should appear showing the document domain.

8. Verification Steps

After sending the HTTP request, verify the injection via WP-CLI:

wp option get wpzoom-portfolio-settings --format=json

Check the output for the presence of the <script> or <img> tags within the readMoreLabel or viewAllLabel keys.

9. Alternative Approaches

  • CSS Injection: If the readMoreLabel is escaped but CSS values are not, target the primaryColor attribute.
    • Payload: #0BB4AA; background-image: url("javascript:alert(1)")
    • Note: The sanitize_css_value function in index.php strips <>\{\}\\, but might be bypassed if the output is placed inside a style="..." attribute rather than a <style> block.
  • AJAX Vector: If the REST API is restricted, attempt an unauthenticated AJAX call to wp_ajax_nopriv_wpzoom_load_more_items (found in index.php) and check if the returned HTML reflects any injected attributes.

Check if your site is affected.

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