Name Directory <= 1.32.1 - Unauthenticated Stored Cross-Site Scripting via 'name_directory_name'
Description
The Name Directory plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'name_directory_name' parameter in all versions up to, and including, 1.32.1 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. The vulnerability was partially patched in versions 1.30.3 and 1.32.1.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=1.32.1What Changed in the Fix
Changes introduced in v1.33.0
Source Code
WordPress.org SVNThis research plan outlines the steps required to demonstrate a Stored Cross-Site Scripting (XSS) vulnerability in the Name Directory plugin (<= 1.32.1). ### 1. Vulnerability Summary The **Name Directory** plugin allows unauthenticated users to submit suggestions to a directory if the "suggestion f…
Show full research plan
This research plan outlines the steps required to demonstrate a Stored Cross-Site Scripting (XSS) vulnerability in the Name Directory plugin (<= 1.32.1).
1. Vulnerability Summary
The Name Directory plugin allows unauthenticated users to submit suggestions to a directory if the "suggestion form" feature is enabled. The parameter name_directory_name used in this submission process is not sufficiently sanitized before being stored in the database and is not properly escaped when rendered in the administrative dashboard or front-end directory listing. This allows an unauthenticated attacker to inject malicious scripts that execute in the context of an administrator viewing the directory's name list.
2. Attack Vector Analysis
- Endpoint: Front-end page containing the Name Directory shortcode.
- Hook: Typically processed via
initor during the execution of the[namedirectory]shortcode (located inshortcode.php, which is referenced inindex.php). - Vulnerable Parameter:
name_directory_name. - Authentication: None (Unauthenticated).
- Preconditions: A directory must exist with the
show_submit_formoption enabled.
3. Code Flow
- Input Submission: An unauthenticated user submits a form on the front-end. The form sends a POST request containing
name_directory_name. - Data Processing: The plugin (likely in
shortcode.php) receives the POST data. It fails to applysanitize_text_field()orwp_kses()to thename_directory_namevalue. - Storage: The raw or insufficiently sanitized value is inserted into the
$name_directory_table_directory_namedatabase table (defined inindex.php). - Rendering (Admin): An administrator navigates to Name Directory > Manage (handled by
name_directory_names()inadmin.php). - Execution: The function retrieves entries from the database and echoes the
namefield directly into the HTML table without usingesc_html(), triggering the XSS payload.
4. Nonce Acquisition Strategy
While some versions of the suggestion form may be entirely unprotected, others use a nonce to prevent spam.
- Identify Shortcode: The
readme.txtspecifies the shortcode format as[namedirectory dir=1]. - Create Test Page: Create a public page containing this shortcode to render the submission form.
- Scrape Nonce:
- Use
browser_navigateto visit the page. - Use
browser_evalto extract the nonce from the form. - Common nonce field names in this plugin:
_wpnonce,name_directory_nonce, orname_directory_suggestion_nonce. - Check for localized JS variables:
window.name_directory_vars(inferred).
- Use
5. Exploitation Strategy
- Preparation: Use WP-CLI to ensure a directory exists and has suggestions enabled.
- Form Discovery: Navigate to the directory page and inspect the form structure.
- Payload Delivery: Send an unauthenticated HTTP POST request to the page URL.
- URL: The URL of the page where the shortcode is placed.
- Content-Type:
application/x-www-form-urlencoded. - Parameters:
name_directory_name:<script>alert(document.domain)</script>name_directory_description:Vulnerable Entryname_directory_directory:1(The ID of the directory)name_directory_submit_suggestion:1(Trigger for the processing logic)_wpnonce: (The nonce value extracted in step 4, if present)
- Trigger: Navigate to the WordPress admin panel as an administrator:
/wp-admin/admin.php?page=name-directory&sub=manage-directory&dir_id=1.
6. Test Data Setup
Execute the following WP-CLI commands to set up the environment:
# 1. Create a directory manually in the database to ensure it exists
wp db query "INSERT INTO wp_name_directory (name, description, show_submit_form, published) VALUES ('Exploit Test', 'Test Directory', 1, 1)"
# 2. Get the ID of the created directory (likely 1)
DIR_ID=$(wp db query "SELECT id FROM wp_name_directory ORDER BY id DESC LIMIT 1" --silent --skip-column-names)
# 3. Create a page with the shortcode
wp post create --post_type=page --post_title="Directory Page" --post_status=publish --post_content="[namedirectory dir=$DIR_ID]"
7. Expected Results
- The POST request should return a success message (e.g., "Thank you for your suggestion" or a redirect).
- The malicious payload
<script>alert(document.domain)</script>should be visible in the database. - When an admin views the names in the directory, a JavaScript alert box should appear.
8. Verification Steps
After sending the exploit request, verify the storage of the payload using WP-CLI:
# Check if the payload exists in the directory names table
wp db query "SELECT name FROM wp_name_directory_name WHERE name LIKE '%<script>%' "
To verify the administrative trigger:
- Use
browser_navigateto visit/wp-admin/admin.php?page=name-directory&sub=manage-directory&dir_id=1. - Use
browser_evalto check if a specific "canary" object injected by the XSS exists or if the script tag is present in the DOM.
9. Alternative Approaches
- Description Field: If
name_directory_nameis sanitized, attempt the exploit vianame_directory_description, as thereadme.txtconfirms search and display of descriptions. - AJAX Action: If the form submits via AJAX, use the
wp_ajax_nopriv_name_directory_submit_name(inferred action name) endpoint at/wp-admin/admin-ajax.php. - Submitter Name: If
show_submitter_nameis enabled in settings, use thename_directory_submitter_nameparameter as an injection point.
Summary
The Name Directory plugin for WordPress is vulnerable to unauthenticated Stored Cross-Site Scripting because the suggestion form does not sufficiently sanitize the 'name_directory_name' parameter and the administrative dashboard fails to escape the output, instead using html_entity_decode() to render entries. An attacker can inject arbitrary JavaScript into a directory suggestion which executes in the context of an administrator viewing the directory's management page.
Vulnerable Code
// admin.php lines 927-930 (v1.32.1) <a class='button button-small' href='" . $wp_url_path . "&delete_name=%d&secnonce=%s'>%s</a> </td><td>%s</td> </tr>", html_entity_decode(stripslashes($name->name)), html_entity_decode(stripslashes($name->description)), --- // helpers.php lines 600-608 (v1.32.1) function name_directory_deep_sanitize_public_user_input($input, $allowed_tags = null) { $raw = trim( wp_unslash( (string)$input ) ); if( ! is_array( $allowed_tags ) ) { $allowed_tags = array('p' => array(), 'br' => array(), 'strong'=>array(), 'em'=>array()); } return wp_kses( $raw, $allowed_tags ); }
Security Fix
@@ -60,7 +60,7 @@ $sub_page = ''; if( ! empty( $_GET['sub'] ) ) { - $sub_page = wp_unslash($_GET['sub']); + $sub_page = sanitize_text_field($_GET['sub']); } switch( $sub_page ) @@ -113,7 +113,7 @@ } $wp_file = admin_url('admin.php'); - $wp_page = $_GET['page']; + $wp_page = sanitize_text_field($_GET['page']); $wp_url_path = sprintf("%s?page=%s", $wp_file, $wp_page); $wp_new_url = sprintf("%s&sub=%s", $wp_url_path, 'new-directory'); $wp_nonce = wp_create_nonce('name-directory-action'); @@ -321,8 +321,8 @@ global $name_directory_table_directory; $wp_file = admin_url('admin.php'); - $wp_page = $_GET['page']; - $wp_sub = $_GET['sub']; + $wp_page = sanitize_text_field($_GET['page']); $wp_sub = sanitize_text_field($_GET['sub']); $overview_url = sprintf("%s?page=%s", $wp_file, $wp_page, $wp_sub); $wp_url_path = sprintf("%s?page=%s", $wp_file, $wp_page); @@ -656,8 +656,8 @@ } $wp_file = admin_url('admin.php'); - $wp_page = $_GET['page']; - $wp_sub = $_GET['sub']; + $wp_page = sanitize_text_field($_GET['page']); + $wp_sub = sanitize_text_field($_GET['sub']); $overview_url = sprintf("%s?page=%s", $wp_file, $wp_page); if(! array_key_exists('dir', $_GET)) { @@ -927,8 +927,8 @@ <a class='button button-small' href='" . $wp_url_path . "&delete_name=%d&secnonce=%s'>%s</a> </td><td>%s</td> </tr>", - html_entity_decode(stripslashes($name->name)), - html_entity_decode(stripslashes($name->description)), + esc_html(stripslashes($name->name)), + esc_html(stripslashes($name->description)), sanitize_text_field(esc_html($name->submitted_by)), $name->id, $name->id, @@ -1014,7 +1014,7 @@ array('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%s', '%s', '%d', '%d') ); - $import_url = sprintf("%s?page=%s&sub=%s&dir=%d", admin_url('admin.php'), $_GET['page'], 'import', $wpdb->insert_id); + $import_url = sprintf("%s?page=%s&sub=%s&dir=%d", admin_url('admin.php'), sanitize_text_field($_GET['page']), 'import', $wpdb->insert_id); echo "<div class='updated'><p>" . sprintf(__('Directory %s created.', 'name-directory'), "<i>" . $cleaned_name . "</i>") @@ -1027,7 +1027,7 @@ $wp_nonce = wp_create_nonce('name-directory-quick'); ?> - <form name="add_name" method="post" action="<?php echo sprintf("%s?page=%s&sub=quick-import&quicknonce=%s", admin_url('admin.php'), $_GET['page'], $wp_nonce); ?>"> + <form name="add_name" method="post" action="<?php echo sprintf("%s?page=%s&sub=quick-import&quicknonce=%s", admin_url('admin.php'), sanitize_text_field($_GET['page']), $wp_nonce); ?>"> <table class="wp-list-table widefat" cellpadding="0"> <thead> <tr> @@ -1179,8 +1179,8 @@ } $wp_file = admin_url('admin.php'); - $wp_page = $_GET['page']; - $wp_sub = $_GET['sub']; + $wp_page = sanitize_text_field($_GET['page']); + $wp_sub = sanitize_text_field($_GET['sub']); $overview_url = sprintf("%s?page=%s", $wp_file, $wp_page); $wp_url_path = sprintf("%s?page=%s&sub=%s&dir=%d", $wp_file, $wp_page, $wp_sub, $directory_id); $wp_ndir_path = sprintf("%s?page=%s&sub=%s&dir=%d", $wp_file, $wp_page, 'manage-directory', $directory_id); @@ -600,12 +600,16 @@ * @return mixed */ function name_directory_deep_sanitize_public_user_input($input, $allowed_tags = null) { + $raw = trim( wp_unslash( (string)$input ) ); + $decoded = html_entity_decode( $raw, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); + if( ! is_array( $allowed_tags ) ) { $allowed_tags = array('p' => array(), 'br' => array(), 'strong'=>array(), 'em'=>array()); } - return wp_kses( $raw, $allowed_tags ); + + return wp_kses( $decoded, $allowed_tags ); } function name_directory_get_html_tag_options() {
Exploit Outline
1. Identify a public-facing page using the [namedirectory] shortcode where the 'show_submit_form' option is enabled. 2. Locate the suggestion submission form and extract the submission nonce if required (e.g., from the hidden '_wpnonce' or 'name_directory_suggestion_nonce' fields). 3. Prepare a malicious payload containing JavaScript, such as '<script>alert(document.domain)</script>'. 4. Submit an unauthenticated HTTP POST request to the directory page with the following parameters: 'name_directory_name' (containing the payload), 'name_directory_description', 'name_directory_directory' (the directory ID), and 'name_directory_submit_suggestion'. 5. Once the suggestion is submitted, an administrator must log in and navigate to the directory management page (wp-admin/admin.php?page=name-directory&sub=manage-directory&dir_id=[ID]). 6. The browser will execute the stored script because the admin interface uses html_entity_decode() to display the name and description fields without proper HTML escaping.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.