The Exploit
An unauthenticated attacker can inject arbitrary JavaScript into the WordPress admin dashboard by crafting a malicious URL to the plugin's reports page. No authentication or user interaction beyond clicking a link is required.
GET /wp-admin/admin.php?page="><script>alert('XSS via page parameter')</script> HTTP/1.1
Host: target.wordpress.local
User-Agent: Mozilla/5.0
Cookie: wordpress_logged_in=... (admin session)
When an authenticated admin visits this link, the injected script executes in their browser within the admin context. The page renders a heading with the unescaped page parameter value reflected in the DOM. An attacker observes the JavaScript alert fire, confirming arbitrary code execution. By replacing the alert with a credential-stealing payload or nonce-hijacking code, the attacker can compromise the admin account outright.
What the Patch Did
Before:
<h2><?php echo wp_slimstat_admin::$screens_info[$_GET['page']]['title'] ?></h2>
After:
<h2><?php echo isset($_GET['page']) && isset(wp_slimstat_admin::$screens_info[sanitize_key($_GET['page'])]) ? esc_html(wp_slimstat_admin::$screens_info[sanitize_key($_GET['page'])]['title']) : '' ?></h2>
The patch applies three defense layers. First, isset() checks guard array access to prevent undefined index warnings. Second, sanitize_key() normalizes the $_GET['page'] parameter to lowercase alphanumeric characters, removing or neutralizing special characters. Third, esc_html() escapes the title string before output, converting <, >, &, and quotes to HTML entities so the browser renders them as text, not markup. The load-bearing control is esc_html() — without it, sanitize_key() alone cannot prevent XSS because the sanitized value is only the array key; the title value still flows unescaped to output.
Root Cause
CWE-79 (Improper Neutralization of Input During Web Page Generation — 'Cross-site Scripting').
The $_GET['page'] parameter is user-controlled and unsanitized. The plugin uses this value to index into wp_slimstat_admin::$screens_info, retrieves the title field, and echoes it directly to the page without escaping. An attacker can inject characters that break out of the HTML context—such as "><script>alert(1)</script><span x=" —and the browser parser will execute the script. The trust boundary is crossed at the echo statement: attacker input transitions from the request into the response body without neutralization.
Why It Works
The critical line is esc_html($wp_slimstat_admin::$screens_info[sanitize_key($_GET['page'])]['title']). Without esc_html(), an attacker's crafted page value could still reach the title via legitimate array keys in the plugin's configuration—for example, if the config contains a key like search">, the title might already be tainted. esc_html() is load-bearing because it guarantees that whatever string emerges from the array is neutralized before rendering. The sanitize_key() wrapper serves a secondary purpose: it prevents attackers from probing the plugin's internal structure by injecting exotic characters into the array key lookup. Removing sanitize_key() leaves the array access vulnerable to enumeration or bypass tricks. Removing isset() checks causes PHP notices that, while not directly exploitable, mask the vulnerability and complicate monitoring. Together, they form defense-in-depth, but only esc_html() stops XSS.
Hardening Checklist
-
Use
esc_html()or context-specific escape functions (esc_attr(),esc_url(),wp_json_encode()) at every output point, not just one layer. Identify the rendering context—HTML text, attribute value, URL, or JSON—and apply the matching escape. -
Apply
sanitize_text_field()orwp_kses_post()to user-supplied content at input time, in addition to output escaping.sanitize_text_field()strips tags and harmful entities;wp_kses_post()allows safe HTML. Use neither as a substitute for output escaping, but as a complementary check. -
Validate
$_GETand$_POSTparameters against a whitelist of expected values usingin_array()or enums when the input is meant to select from a fixed set (e.g., page slugs, report types). This reduces the attack surface before escaping. -
Audit all array accesses and function calls that receive
$_GET,$_POST, or$_REQUESTvalues. Use a grep or static analyzer to find patterns likeecho $_GET,[$_POST['key']], orfunction($_GET['param']). Each requires validation and escaping. -
Enforce a Content Security Policy (CSP) header in the plugin's admin pages (
script-src 'self') to mitigate stored and reflected XSS even if escaping is missed. This is a backstop, not a primary control.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-69323