CVE-2026-3178

Name Directory <= 1.32.1 - Unauthenticated Stored Cross-Site Scripting via 'name_directory_name'

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
1.33.0
Patched in
1d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.32.1
PublishedMarch 10, 2026
Last updatedMarch 11, 2026
Affected pluginname-directory

What Changed in the Fix

Changes introduced in v1.33.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

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 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 init or during the execution of the [namedirectory] shortcode (located in shortcode.php, which is referenced in index.php).
  • Vulnerable Parameter: name_directory_name.
  • Authentication: None (Unauthenticated).
  • Preconditions: A directory must exist with the show_submit_form option enabled.

3. Code Flow

  1. Input Submission: An unauthenticated user submits a form on the front-end. The form sends a POST request containing name_directory_name.
  2. Data Processing: The plugin (likely in shortcode.php) receives the POST data. It fails to apply sanitize_text_field() or wp_kses() to the name_directory_name value.
  3. Storage: The raw or insufficiently sanitized value is inserted into the $name_directory_table_directory_name database table (defined in index.php).
  4. Rendering (Admin): An administrator navigates to Name Directory > Manage (handled by name_directory_names() in admin.php).
  5. Execution: The function retrieves entries from the database and echoes the name field directly into the HTML table without using esc_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.

  1. Identify Shortcode: The readme.txt specifies the shortcode format as [namedirectory dir=1].
  2. Create Test Page: Create a public page containing this shortcode to render the submission form.
  3. Scrape Nonce:
    • Use browser_navigate to visit the page.
    • Use browser_eval to extract the nonce from the form.
    • Common nonce field names in this plugin: _wpnonce, name_directory_nonce, or name_directory_suggestion_nonce.
    • Check for localized JS variables: window.name_directory_vars (inferred).

5. Exploitation Strategy

  1. Preparation: Use WP-CLI to ensure a directory exists and has suggestions enabled.
  2. Form Discovery: Navigate to the directory page and inspect the form structure.
  3. 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 Entry
      • name_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)
  4. 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_navigate to visit /wp-admin/admin.php?page=name-directory&sub=manage-directory&dir_id=1.
  • Use browser_eval to 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_name is sanitized, attempt the exploit via name_directory_description, as the readme.txt confirms 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_name is enabled in settings, use the name_directory_submitter_name parameter as an injection point.
Research Findings
Static analysis — not yet PoC-verified

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

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/name-directory/1.32.1/admin.php /home/deploy/wp-safety.org/data/plugin-versions/name-directory/1.33.0/admin.php
--- /home/deploy/wp-safety.org/data/plugin-versions/name-directory/1.32.1/admin.php	2026-02-05 23:17:02.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/name-directory/1.33.0/admin.php	2026-03-09 23:59:50.000000000 +0000
@@ -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);
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/name-directory/1.32.1/helpers.php /home/deploy/wp-safety.org/data/plugin-versions/name-directory/1.33.0/helpers.php
--- /home/deploy/wp-safety.org/data/plugin-versions/name-directory/1.32.1/helpers.php	2026-02-05 23:17:02.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/name-directory/1.33.0/helpers.php	2026-03-09 23:59:50.000000000 +0000
@@ -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.