WPZOOM Portfolio Lite – Filterable Portfolio Plugin <= 1.4.29 - Unauthenticated Stored Cross-Site Scripting
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:NTechnical Details
<=1.4.29What Changed in the Fix
Changes introduced in v1.4.30
Source Code
WordPress.org SVNThis 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 fromWPZOOM_Blocks::rest_api_routes) or the AJAX actionwpzoom_portfolio_save_settings. - Target Parameter: Settings fields such as
viewAllLabel,readMoreLabel, orbackgroundColor. - Authentication: Unauthenticated (the
permission_callbackfor the REST route is likely missing or returnstrue). - 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
- Entry Point: An unauthenticated user sends a
POSTrequest to/wp-json/wpzoom-portfolio/v1/settings. - Lack of Authorization: The
WPZOOM_Blocks::rest_api_routes()method (registered inwpzoom-portfolio.php) defines a route that lacks a restrictivepermission_callback. - Storage: The handler (likely
update_settings) takes the JSON input and updates the WordPress optionwpzoom-portfolio-settingsusingupdate_option(). - Processing: The values are stored raw. While
WPZOOM_Blocks_Portfolio::sanitize_css_valueexists inbuild/blocks/portfolio/index.php, it is only applied to specific CSS fields and is bypassable for HTML contexts. - 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 scriptwpzoom-blocks-js-index-mainor echoed directly in the PHP template. - 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.
- Identify Shortcode: The plugin uses the shortcode
[wpzoom_portfolio]and the blockwpzoom-blocks/portfolio. - Create Trigger Page:
wp post create --post_type=page --post_title="Portfolio Gallery" --post_status=publish --post_content='[wpzoom_portfolio]' - Extract Nonce:
Navigate to the newly created page and usebrowser_evalto extract the REST nonce, which WordPress often exposes in thewpApiSettingsobject or localized plugin data.
Note: If the REST endpoint is truly unauthenticated and bypasses the standard WP REST auth check, a nonce may not be required.// Extract from WP default localized data window.wpApiSettings?.nonce || window.wpzoomPortfolioBlock?.nonce
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/jsonX-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
- Install Plugin: Ensure WPZOOM Portfolio Lite version 1.4.29 is active.
- 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 - 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 OKor201 Createdresponse. - 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
primaryColororbackgroundColorattributes. Thesanitize_css_valuefunction only strips<>{}\, which might allow for CSS-based exfiltration or breakout if the output context is an inlinestyleattribute. - 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_filteredor via a dedicatedwpzoom-portfolio/v1/layoutendpoint. - AJAX Fallback: Check for
wp_ajax_nopriv_wpzoom_portfolio_save_settingsinwp-admin/admin-ajax.phpif the REST API is disabled.
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
@@ -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.