CVE-2026-4006

Draft List <= 2.6.2 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'display_name' Parameter

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

Description

The Simple Draft List plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'display_name' post meta (Custom Field) in all versions up to and including 2.6.2. This is due to insufficient input sanitization and output escaping on the author display name when no author URL is present. The plugin accesses `$draft_data->display_name` which, because `display_name` is not a native WP_Post property, triggers WP_Post::__get() and resolves to `get_post_meta($post_id, 'display_name', true)`. When the `user_url` meta field is empty, the `$author` value is assigned to `$author_link` on line 383 without any escaping (unlike line 378 which uses `esc_html()` for the `{{author}}` tag, and line 381 which uses `esc_html()` when a URL is present). This unescaped value is then inserted into the shortcode output via `str_replace()`. 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 a page containing the `[drafts]` shortcode with the `{{author+link}}` template tag.

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.2
PublishedMarch 18, 2026
Last updatedMarch 19, 2026
Affected pluginsimple-draft-list

What Changed in the Fix

Changes introduced in v2.6.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Research Plan: CVE-2026-4006 - Stored XSS via `display_name` Post Meta ## 1. Vulnerability Summary The **Draft List** plugin (<= 2.6.2) is vulnerable to Stored Cross-Site Scripting (XSS) due to insufficient output escaping in its shortcode rendering logic. Specifically, the plugin attempts to dis…

Show full research plan

Research Plan: CVE-2026-4006 - Stored XSS via display_name Post Meta

1. Vulnerability Summary

The Draft List plugin (<= 2.6.2) is vulnerable to Stored Cross-Site Scripting (XSS) due to insufficient output escaping in its shortcode rendering logic. Specifically, the plugin attempts to display an author's name using the {{author+link}} template tag. When processing this tag, the plugin accesses a property $draft_data->display_name. Because WP_Post objects do not natively have a display_name property, WordPress's magic __get() method fetches the value from the post meta table (get_post_meta($post_id, 'display_name', true)).

An authenticated attacker with Contributor-level permissions can create a draft and set a custom field named display_name containing a malicious script. When a user (including an Administrator) views a page containing the [drafts] shortcode with the {{author+link}} template tag, the script executes because the plugin fails to escape the meta-sourced value when no author URL is associated with it.

2. Attack Vector Analysis

  • Endpoint: wp-admin/post.php (to store the payload) and any frontend page containing the [drafts] shortcode (to trigger the payload).
  • Vulnerable Parameter: meta_input[display_name] (Custom Field/Post Meta).
  • Authentication Level: Authenticated Contributor or higher. Contributors can create posts and manage their own post meta.
  • Preconditions:
    1. The attacker must be able to create or edit a post (Contributor role).
    2. A page or widget must exist that uses the [drafts] shortcode.
    3. The shortcode must use a template containing the {{author+link}} tag.

3. Code Flow

  1. Entry Point: A user views a page with the shortcode [drafts].
  2. Shortcode Handling: draft_list_shortcode() in inc/create-lists.php is called.
  3. List Generation: draft_list_shortcode() calls draft_list_generate_code().
  4. Data Retrieval: draft_list_generate_code() fetches draft posts. Each post is returned as a WP_Post object (stored in $draft_data).
  5. Template Processing: The code iterates through the template strings.
  6. Property Access: The code accesses $draft_data->display_name.
  7. Magic Getter: WP_Post::__get('display_name') executes get_post_meta($post_id, 'display_name', true).
  8. The Sink: In inc/create-lists.php (around line 383), if the author's URL is empty, the variable $author_link is assigned the value of $author (which holds the meta value) without passing through esc_html() or esc_attr().
  9. Rendering: The unescaped $author_link is inserted into the final HTML output via str_replace() and returned to the browser.

4. Nonce Acquisition Strategy

Storing the Payload (Post Meta)

To store the malicious payload as a Contributor, the attacker needs to update a post's meta. This is typically done via the wp-admin/post.php endpoint.

  1. Action: editpost
  2. Nonce: The _wpnonce is required for authorized post updates.
  3. Acquisition:
    • Use browser_navigate to go to wp-admin/post-new.php.
    • Use browser_eval to extract the nonce from the document:
      browser_eval("document.querySelector('#_wpnonce').value").

Triggering the Payload (Frontend)

No nonce is required to view the frontend output of a shortcode.

5. Exploitation Strategy

Step 1: Create a Draft Post and Inject Payload

  1. Log in as a Contributor.
  2. Navigate to wp-admin/post-new.php.
  3. Extract the _wpnonce and the newly generated post_ID.
  4. Send a POST request to wp-admin/post.php to save the malicious meta:
    • URL: http://localhost:8080/wp-admin/post.php
    • Method: POST
    • Headers: Content-Type: application/x-www-form-urlencoded
    • Body:
      action=editpost
      post_ID=[ID]
      _wpnonce=[NONCE]
      post_title=XSS-Draft
      post_type=post
      post_status=draft
      meta_input[display_name]=<img src=x onerror=alert("CVE-2026-4006")>
      

Step 2: Setup the Trigger Page

  1. Log in as an Administrator.
  2. Create a public page that renders the draft list with the vulnerable template tag.
    • Action: wp post create --post_type=page --post_title="Draft List Test" --post_status=publish --post_content='[drafts template="{{author+link}}{{draft}}"]'

Step 3: Trigger Execution

  1. Navigate to the newly created "Draft List Test" page.
  2. The browser will render the list of drafts.
  3. When it reaches the attacker's draft, it will fetch the display_name meta and inject the <img ...> tag into the DOM.

6. Test Data Setup

  • Users:
    • contributor_user: Role contributor
    • admin_user: Role administrator
  • Posts:
    • One draft post created by contributor_user with meta key display_name set to <img src=x onerror=alert(document.domain)>.
  • Shortcode Page:
    • A page containing [drafts template="{{author+link}}{{draft}}"].

7. Expected Results

  • The HTTP response from the frontend page will contain the literal, unescaped string: <img src=x onerror=alert("CVE-2026-4006")>.
  • In a browser context, the JavaScript alert will execute.

8. Verification Steps

  1. Verify Meta Storage:
    wp post meta get [POST_ID] display_name
    • Should return the XSS payload.
  2. Verify Unescaped Output:
    Use http_request to fetch the frontend page and grep for the payload.
    grep '<img src=x onerror=alert("CVE-2026-4006")>' 
    
  3. Verify Context:
    Confirm that the {{author+link}} tag was replaced by the payload without any HTML entity encoding (e.g., no &lt;).

9. Alternative Approaches

  • Template Parameter Abuse: If the attacker cannot edit a page to add the shortcode, they can attempt to find a widget or an existing page that uses [drafts] and rely on the fact that any post they create (as a draft) will be pulled into that list automatically if it meets the shortcode's criteria (like limit or type).
  • Shortcode Injection: If the site allows Contributors to use unfiltered_html (rare) or if there is another way to place shortcodes, the attacker can provide the template attribute directly in the shortcode: [drafts template="{{author+link}}"].
  • Author URL bypass: If the vulnerability logic requires the user_url to be empty, ensure that the Contributor user profile has an empty Website field in their WordPress profile, OR verify that the plugin is looking for a user_url post meta (which will be empty by default for a new post). The vulnerability description specifically mentions $author_link is assigned $author on line 383 when the URL is empty.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Draft List plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'display_name' post meta in versions up to 2.6.2. Authenticated contributors can inject malicious scripts into the 'display_name' custom field, which are then executed when a user views a page containing the [drafts] shortcode with the {{author+link}} template tag because the plugin fails to escape the meta-derived value when no author URL is present.

Vulnerable Code

// inc/create-lists.php line 380

					if ( '' !== $author_url ) {
						$author_link = '<a href="' . esc_url( $author_url ) . '">' . esc_html( $author ) . '</a>';
					} else {
						$author_link = $author;
					}
					$this_line = str_replace( '{{author+link}}', $author_link, $this_line );

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/simple-draft-list/2.6.2/inc/create-lists.php /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.2/inc/create-lists.php	2025-10-10 16:00:22.000000000 +0000
+++ /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
@@ -380,7 +380,7 @@
 					if ( '' !== $author_url ) {
 						$author_link = '<a href="' . esc_url( $author_url ) . '">' . esc_html( $author ) . '</a>';
 					} else {
-						$author_link = $author;
+						$author_link = esc_html( $author );
 					}
 					$this_line = str_replace( '{{author+link}}', $author_link, $this_line );

Exploit Outline

1. Log in as a Contributor or higher level user. 2. Create or edit a draft post. 3. Add a Post Meta (Custom Field) entry with the key 'display_name' and a value containing a malicious script, such as: <img src=x onerror=alert(document.domain)>. 4. Ensure the current user's WordPress profile has an empty 'Website' (user_url) field, or that no user_url meta exists for the post. 5. Navigate to or create a page that includes the plugin's shortcode with a template tag referencing the author link, e.g., [drafts template="{{author+link}}{{draft}}"]. 6. When any user (including an administrator) visits the page, the plugin retrieves the 'display_name' meta via magic getter, fails to escape it in the 'else' block of the link generation logic, and outputs the raw script into the page HTML.

Check if your site is affected.

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