CVE-2026-9022

Splide Carousel Block <= 1.7.1 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'url' Block Attribute

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

Description

The Splide Carousel Block plugin for WordPress is vulnerable to Stored Cross-Site Scripting via 'url' Block Attribute in all versions up to, and including, 1.7.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. The injected payload must be published before it executes for site visitors, which requires an editor or administrator to approve and publish the contributor's post.

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.7.1
PublishedMay 26, 2026
Last updatedMay 27, 2026
Affected pluginsplide-carousel

What Changed in the Fix

Changes introduced in v1.7.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-9022 ## 1. Vulnerability Summary The **Splide Carousel Block** plugin (versions <= 1.7.1) is vulnerable to **Authenticated Stored Cross-Site Scripting (XSS)**. The vulnerability exists in the `cloudcatch/splide-carousel-item` block, specifically within the `u…

Show full research plan

Exploitation Research Plan - CVE-2026-9022

1. Vulnerability Summary

The Splide Carousel Block plugin (versions <= 1.7.1) is vulnerable to Authenticated Stored Cross-Site Scripting (XSS). The vulnerability exists in the cloudcatch/splide-carousel-item block, specifically within the url attribute. While the editor UI attempts to prepend protocols, it does not sanitize or validate against the javascript: protocol. Furthermore, the block's save function stores this attribute in a JSON string within a data-link attribute on the frontend. When a user clicks the carousel item, the frontend JavaScript parses this JSON and executes the URL, leading to script execution if a javascript: URI is used.

2. Attack Vector Analysis

  • Endpoint: WordPress REST API Post creation/update (/wp-json/wp/v2/posts) or the Gutenberg Editor (/wp-admin/post.php).
  • Vulnerable Block: cloudcatch/splide-carousel-item.
  • Vulnerable Attribute: url.
  • Payload Type: javascript: protocol URI.
  • Authentication Level: Contributor or higher. Contributors can create and save posts (stored XSS), which then execute when viewed by an Editor or Administrator (e.g., during post review).
  • Preconditions: The plugin must be active, and the attacker must have credentials for a Contributor-level account.

3. Code Flow

  1. Input: A Contributor user creates or edits a post and inserts a cloudcatch/splide-carousel-item block with a malicious url attribute (e.g., javascript:alert(window.origin)).
  2. Editor Processing (build/carousel-item/index.js):
    • The edit function uses (0, g.prependHTTP)(e) to process the URL.
    • prependHTTP (from wp-url) allows any string starting with a protocol (matching [a-z]+:), thus permitting javascript:.
  3. Storage (build/carousel-item/index.js):
    • The save function generates the frontend HTML:
      "data-link": c ? JSON.stringify({ url: i, target: o, rel: a }) : null
      
    • The attribute i (the raw url) is placed into a JSON object and stringified into the data-link attribute.
  4. Frontend Execution (build/carousel/view.js):
    • The frontend script initializes the Splide carousel.
    • It retrieves the data-link attribute from the slide element.
    • Upon a click event, it parses the JSON and performs a navigation/redirection using the url property (e.g., window.location.href = data.url or window.open(data.url)).
  5. Sink: The browser executes the javascript: URI, resulting in XSS.

4. Nonce Acquisition Strategy

To interact with the REST API for post creation, a REST Nonce (_wpnonce) is required.

  1. Login: Authenticate as a Contributor using the browser_navigate tool.
  2. Navigation: Navigate to the "New Post" screen: /wp-admin/post-new.php.
  3. Extraction: Use browser_eval to extract the nonce from the wpApiSettings object which is globally available in the Gutenberg editor context.
    • JavaScript: window.wpApiSettings.nonce

5. Exploitation Strategy

  1. Setup Account: Ensure a Contributor user exists.
  2. Get Nonce: Login as Contributor and extract the REST API nonce from /wp-admin/post-new.php.
  3. Create Malicious Post: Use the http_request tool to send a POST request to /wp-json/wp/v2/posts.
    • Headers:
      • Content-Type: application/json
      • X-WP-Nonce: [EXTRACTED_NONCE]
    • Payload:
      {
        "title": "XSS Carousel Test",
        "content": "<!-- wp:cloudcatch/splide-carousel -->\n<div class=\"wp-block-cloudcatch-splide-carousel\">\n<!-- wp:cloudcatch/splide-carousel-item {\"url\":\"javascript:alert(document.domain)\"} -->\n<div class=\"wp-block-cloudcatch-splide-carousel-item\"></div>\n<!-- /wp:cloudcatch/splide-carousel-item -->\n</div>\n<!-- /wp:cloudcatch/splide-carousel -->",
        "status": "draft"
      }
      
  4. Obtain Post URL: Extract the link from the REST API response.
  5. Trigger XSS:
    • Navigate to the post's preview URL or the published URL (if an admin publishes it).
    • Click on the carousel slide item.

6. Test Data Setup

  1. Plugin: Install and activate splide-carousel version 1.7.1.
  2. User: Create a user with the contributor role.
  3. Environment: Ensure the WordPress instance is configured to allow blocks (standard in WP 5.0+).

7. Expected Results

  • The REST API call should return 201 Created.
  • The post content in the database (verified via wp post get [ID] --field=post_content) should contain: <!-- wp:cloudcatch/splide-carousel-item {"url":"javascript:alert(document.domain)"} -->.
  • The frontend HTML for the slide should contain: data-link='{"url":"javascript:alert(document.domain)", ...}'.
  • Upon clicking the slide on the frontend, an alert box showing the document domain should appear.

8. Verification Steps

  1. Database Check:
    • wp post list --post_type=post --post_status=draft
    • wp post get [POST_ID] --field=post_content
  2. Frontend Render Check:
    • Navigate to the post URL.
    • Inspect the HTML for the element with class splide__slide.
    • Confirm the presence of data-link containing the javascript: payload.

9. Alternative Approaches

  • Bypassing prependHTTP: If prependHTTP was more restrictive, an attacker could try data-link attribute breakout:
    • Payload for url: "}><img src=x onerror=alert(1)>
    • This targets the way JSON.stringify output is placed into the HTML attribute if the plugin failed to use esc_attr on the block's save output (though Gutenberg usually handles this).
  • Editor-Side XSS: Check if the url is rendered unsanitized within the Gutenberg Editor (edit function in index.js). If the editor displays the link in an <a> tag or an <iframe> preview, the XSS might trigger for the Contributor immediately upon saving.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Splide Carousel Block plugin is vulnerable to Stored Cross-Site Scripting (XSS) via the 'url' attribute of the Splide Carousel Item block. Authenticated attackers with contributor-level access can inject 'javascript:' URIs into the block, which are stored in a data attribute and executed in the browser context when a user clicks on the carousel slide.

Vulnerable Code

// build/carousel-item/index.js line 101 (edit function processing)
onChange:({url:e="",opensInNewTab:t})=>{x({url:(0,g.prependHTTP)(e)}),A!==t&&(e=>{const t=e?"_blank":void 0;let r=_;t&&!_?r=w:t||_!==w||(r=void 0),x({target:t,rel:r})})(t)}

---

// build/carousel-item/index.js line 109 (save function rendering)
save:function(e){const{attributes:t}=e,{urlHash:r,verticalAlignment:n,url:i,target:o,rel:a}=t,c=!!i,u=p.useBlockProps.save({className:l()("wp-block-splide-carousel__slide","splide__slide",{[`is-vertically-aligned-${n}`]:n}),"data-link":c?JSON.stringify({url:i,target:o,rel:a}):null,...r&&{"data-splide-hash":r}}),d=p.useInnerBlocksProps.save(u);return(0,s.jsx)("div",{...d})}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/splide-carousel/1.7.1/build/carousel/block.json /home/deploy/wp-safety.org/data/plugin-versions/splide-carousel/1.7.2/build/carousel/block.json
--- /home/deploy/wp-safety.org/data/plugin-versions/splide-carousel/1.7.1/build/carousel/block.json	2026-01-17 13:52:28.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/splide-carousel/1.7.2/build/carousel/block.json	2026-05-19 16:12:38.000000000 +0000
@@ -2,7 +2,7 @@
   "$schema": "https://schemas.wp.org/trunk/block.json",
   "apiVersion": 3,
   "name": "cloudcatch/splide-carousel",
-  "version": "1.7.1",
+  "version": "1.7.2",
   "title": "Splide Carousel",
   "category": "media",
   "attributes": {
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/splide-carousel/1.7.1/build/carousel/index.asset.php /home/deploy/wp-safety.org/data/plugin-versions/splide-carousel/1.7.2/build/carousel/index.asset.php
--- /home/deploy/wp-safety.org/data/plugin-versions/splide-carousel/1.7.1/build/carousel/index.asset.php	2026-01-17 13:52:28.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/splide-carousel/1.7.2/build/carousel/index.asset.php	2026-05-19 16:12:38.000000000 +0000
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'react-jsx-runtime', 'wp-blob', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-notices'), 'version' => '51deb1d6947788b25af6');
+<?php return array('dependencies' => array('lodash', 'react-jsx-runtime', 'wp-blob', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-notices'), 'version' => 'e8fd1be94de72804e90b');

Exploit Outline

To exploit this vulnerability, an attacker with Contributor-level credentials (or higher) navigates to the WordPress block editor and inserts a 'Splide Carousel Item' block. The attacker then modifies the 'url' attribute of this block to include a malicious JavaScript payload (e.g., 'javascript:alert(document.domain)'). Because the plugin uses the 'prependHTTP' helper without further validation, it allows the 'javascript:' protocol. When the post is saved and subsequently viewed (either in preview mode or after being published by an editor), the frontend JavaScript retrieves the malicious URL from the 'data-link' attribute and executes it via browser navigation when the carousel slide is clicked.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.