Draft List <= 2.6.2 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'display_name' Parameter
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:NTechnical Details
<=2.6.2What Changed in the Fix
Changes introduced in v2.6.3
Source Code
WordPress.org SVN# 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:
- The attacker must be able to create or edit a post (Contributor role).
- A page or widget must exist that uses the
[drafts]shortcode. - The shortcode must use a template containing the
{{author+link}}tag.
3. Code Flow
- Entry Point: A user views a page with the shortcode
[drafts]. - Shortcode Handling:
draft_list_shortcode()ininc/create-lists.phpis called. - List Generation:
draft_list_shortcode()callsdraft_list_generate_code(). - Data Retrieval:
draft_list_generate_code()fetches draft posts. Each post is returned as aWP_Postobject (stored in$draft_data). - Template Processing: The code iterates through the template strings.
- Property Access: The code accesses
$draft_data->display_name. - Magic Getter:
WP_Post::__get('display_name')executesget_post_meta($post_id, 'display_name', true). - The Sink: In
inc/create-lists.php(around line 383), if the author's URL is empty, the variable$author_linkis assigned the value of$author(which holds the meta value) without passing throughesc_html()oresc_attr(). - Rendering: The unescaped
$author_linkis inserted into the final HTML output viastr_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.
- Action:
editpost - Nonce: The
_wpnonceis required for authorized post updates. - Acquisition:
- Use
browser_navigateto go towp-admin/post-new.php. - Use
browser_evalto extract the nonce from the document:browser_eval("document.querySelector('#_wpnonce').value").
- Use
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
- Log in as a Contributor.
- Navigate to
wp-admin/post-new.php. - Extract the
_wpnonceand the newly generatedpost_ID. - Send a POST request to
wp-admin/post.phpto 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")>
- URL:
Step 2: Setup the Trigger Page
- Log in as an Administrator.
- 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}}"]'
- Action:
Step 3: Trigger Execution
- Navigate to the newly created "Draft List Test" page.
- The browser will render the list of drafts.
- When it reaches the attacker's draft, it will fetch the
display_namemeta and inject the<img ...>tag into the DOM.
6. Test Data Setup
- Users:
contributor_user: Rolecontributoradmin_user: Roleadministrator
- Posts:
- One draft post created by
contributor_userwith meta keydisplay_nameset to<img src=x onerror=alert(document.domain)>.
- One draft post created by
- Shortcode Page:
- A page containing
[drafts template="{{author+link}}{{draft}}"].
- A page containing
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
alertwill execute.
8. Verification Steps
- Verify Meta Storage:
wp post meta get [POST_ID] display_name- Should return the XSS payload.
- Verify Unescaped Output:
Usehttp_requestto fetch the frontend page and grep for the payload.grep '<img src=x onerror=alert("CVE-2026-4006")>' - Verify Context:
Confirm that the{{author+link}}tag was replaced by the payload without any HTML entity encoding (e.g., no<).
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 (likelimitortype). - 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 thetemplateattribute directly in the shortcode:[drafts template="{{author+link}}"]. - Author URL bypass: If the vulnerability logic requires the
user_urlto 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 auser_urlpost meta (which will be empty by default for a new post). The vulnerability description specifically mentions$author_linkis assigned$authoron line 383 when the URL is empty.
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
@@ -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.