Draft List <= 2.6.3 - Authenticated (Author+) Stored Cross-Site Scripting via Draft Post Title
Description
The Draft List plugin for WordPress is vulnerable to Stored Cross-Site Scripting via Draft Post Title in all versions up to, and including, 2.6.3 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. The unescaped injection path is triggered specifically when the viewing user lacks edit capabilities, meaning payloads embedded in draft post titles via attribute-breakout techniques execute for unauthenticated users and subscribers.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
>=2.6.3 <=2.6.3What Changed in the Fix
Changes introduced in v2.6.4
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-9104 (Draft List Plugin) ## 1. Vulnerability Summary The **Draft List** plugin (versions <= 2.6.3) contains a stored cross-site scripting (XSS) vulnerability. The plugin allows authenticated users with **Author** level permissions or higher to inject arbitrar…
Show full research plan
Exploitation Research Plan - CVE-2026-9104 (Draft List Plugin)
1. Vulnerability Summary
The Draft List plugin (versions <= 2.6.3) contains a stored cross-site scripting (XSS) vulnerability. The plugin allows authenticated users with Author level permissions or higher to inject arbitrary web scripts via a draft's post title. This occurs because the plugin fails to sanitize or escape the post title when it is rendered through the [drafts] shortcode, particularly when the viewing user does not have edit_posts capabilities (e.g., Unauthenticated Guests or Subscribers).
2. Attack Vector Analysis
- Entry Point: WordPress Post Editor (
wp-admin/post.php). - Vulnerable Parameter:
post_title. - Authentication Requirement: Authenticated (Author+ role).
- Trigger: Viewing a page/post containing the
[drafts]shortcode as a user without edit permissions. - Preconditions:
- An Author creates a draft with a malicious title.
- A site administrator (or the Author) publishes a page using the
[drafts]shortcode.
3. Code Flow
- Injection: An Author saves a draft post. The
post_titleis stored in thewp_poststable. - Shortcode Execution: A user visits a page with
[drafts]. WordPress callsdraft_list_shortcode()ininc/create-lists.php. - Template Handling:
draft_list_shortcodecallsdraft_list_generate_code(). - Sanitization (Bypassed): In
inc/create-lists.php, the$templateitself is sanitized viawp_kses()on line 150:
However, this only sanitizes the tags in the template string, not the content of the draft post that will be interpolated into it.$template = wp_kses( html_entity_decode( $template ), $allowed_list ); - Sink: The function (in the truncated portion) iterates through drafts. It replaces the
{{draft}}placeholder in the$templatewith the$post->post_title. - Capability Check: The vulnerability description specifies that the unescaped path is triggered when the viewer "lacks edit capabilities." This implies that the logic for non-editors omits escaping functions like
esc_html()oresc_attr()when inserting the title into the HTML string. - Breakout: If the title is placed inside a
titleattribute or a<span>without escaping, a payload like"><script>alert(1)</script>breaks out of the attribute/tag.
4. Nonce Acquisition Strategy
This vulnerability is triggered by viewing a standard page, so no nonce is required for the victim to execute the payload. To inject the payload as an Author, the standard WordPress post-editing nonce is required.
Injection Nonce Acquisition (for Author):
- Log in to the WordPress dashboard as an Author.
- Navigate to
wp-admin/post-new.php. - Use the
browser_evaltool to extract the nonce required for saving the post:document.querySelector('#_wpnonce').value - This nonce is used in the
POSTrequest towp-admin/post.php.
(Note: Based on the provided source, this plugin does not appear to use wp_localize_script for shortcode rendering, as it is a purely server-side generation process.)
5. Exploitation Strategy
Step 1: Inject Stored XSS Payload
The attacker (Author) creates a draft post.
- URL:
http://vulnerable-wp.local/wp-admin/post.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body Parameters:
action:editpostpost_ID: (The ID of a newly created draft)post_title:"><script>alert(document.domain)</script>post_status:draft_wpnonce: (Acquired from Step 4)
Step 2: Create a Trigger Page
The attacker or an admin ensures a page exists with the [drafts] shortcode.
- Shortcode:
[drafts limit=10]
Step 3: Trigger Execution
Access the page as an unauthenticated guest.
- URL:
http://vulnerable-wp.local/page-with-shortcode/ - Mechanism: The plugin renders the list of drafts. Because the guest cannot edit the draft, the plugin enters the vulnerable code path and echoes the
post_titleunsafely.
6. Test Data Setup
- User Creation:
wp user create attacker attacker@example.com --role=author --user_pass=password - Draft Creation:
wp post create --post_type=post --post_status=draft --post_title='Initial Title' --post_author=(ID of attacker) - Shortcode Page:
wp post create --post_type=page --post_status=publish --post_title='Coming Soon' --post_content='[drafts]'
7. Expected Results
- When the guest visits the 'Coming Soon' page, the HTML source will contain:
... <span title=""><script>alert(document.domain)</script>"> ...(or similar depending on the exact attribute used). - A JavaScript
alertbox will appear displaying the domain name.
8. Verification Steps
- Check Database:
wp db query "SELECT post_title FROM wp_posts WHERE post_status='draft'"to confirm the payload is stored. - Verify Response: Use
http_requestas an unauthenticated user to fetch the page content and grep for the raw string"><script>alert. - Escaping Check: If the output shows
"><script>, the vulnerability is patched. If it shows"><script>, it is vulnerable.
9. Alternative Approaches
- Attribute Breakout: If
<script>is filtered by a firewall, use an event handler:post_title = '" onmouseover="alert(1) " '. - Custom Template: If the default template is somehow escaped, an Author might attempt to override it via the shortcode itself:
[drafts template='<div title="{{draft}}">Check this out</div>']. The plugin'swp_ksescall on the template (line 150) may allow thetitleattribute, but it won't prevent the title data from breaking out of that attribute.
Summary
The Draft List plugin (versions <= 2.6.3) is vulnerable to Stored Cross-Site Scripting due to insufficient output escaping of draft post titles within the `[drafts]` shortcode. Authenticated attackers with Author-level permissions or higher can inject malicious scripts into draft titles that execute when any user views a page where the shortcode renders those drafts.
Vulnerable Code
// inc/create-lists.php (approx line 386 in version 2.6.3) // Replace the draft tag. if ( '' !== $draft_title ) { $draft = $draft_title; } else { $draft = __( '(no title)', 'simple-draft-list' ); } if ( $can_edit ) { $draft = '<a href="' . home_url() . '/wp-admin/post.php?post=' . $post_id . '&action=edit" rel="nofollow">' . esc_html( $draft ) . '</a>'; } $this_line = str_replace( '{{draft}}', $draft, $this_line );
Security Fix
@@ -386,12 +386,13 @@ // Replace the draft tag. if ( '' !== $draft_title ) { - $draft = $draft_title; + $draft = esc_html( $draft_title ); } else { - $draft = __( '(no title)', 'simple-draft-list' ); + $draft = esc_html__( '(no title)', 'simple-draft-list' ); } if ( $can_edit ) { - $draft = '<a href="' . home_url() . '/wp-admin/post.php?post=' . $post_id . '&action=edit" rel="nofollow">' . esc_html( $draft ) . '</a>'; + $edit_url = esc_url( admin_url( 'post.php?post=' . absint( $post_id ) . '&action=edit' ) ); + $draft = '<a href="' . $edit_url . '" rel="nofollow">' . $draft . '</a>'; } $this_line = str_replace( '{{draft}}', $draft, $this_line );
Exploit Outline
1. Log in to the WordPress site as a user with Author role permissions or higher. 2. Create a new draft post and set the `post_title` to an XSS payload (e.g., "><script>alert(1)</script>). 3. Ensure a page exists on the site that utilizes the `[drafts]` shortcode. 4. Access the page containing the shortcode as an unauthenticated guest or a subscriber (any user without the 'edit_posts' capability). 5. The plugin renders the draft list, and because the viewer cannot edit the post, it injects the raw, unescaped title into the page's HTML, executing the script.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.