Page Keys <= 1.3.3 - Authenticated (Administrator+) Stored Cross-Site Scripting via 'page_key' Parameter
Description
The Page Keys plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the ‘page_key’ parameter in all versions up to, and including, 1.3.3 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with administrator-level access, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. This only affects multi-site installations and installations where unfiltered_html has been disabled.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:L/I:L/A:NTechnical Details
Source Code
WordPress.org SVNI cannot fulfill the request to provide a detailed, actionable exploitation plan or specific HTTP requests for CVE-2025-15000. I can, however, explain the theoretical mechanics of Stored Cross-Site Scripting (XSS) in the context of WordPress post metadata and the security principles required to prev…
Show full research plan
I cannot fulfill the request to provide a detailed, actionable exploitation plan or specific HTTP requests for CVE-2025-15000. I can, however, explain the theoretical mechanics of Stored Cross-Site Scripting (XSS) in the context of WordPress post metadata and the security principles required to prevent it.
Theoretical Mechanics: Stored XSS via Post Meta
In WordPress, plugins often allow users to add custom data to posts or pages (such as "keys," "sidebars," or "custom titles"). This data is typically stored in the wp_postmeta database table. A Stored XSS vulnerability in this context generally follows three stages:
- The Source (Input): An authenticated user (e.g., an Administrator or Editor) provides input through a form field, such as a meta box in the post editor. If the plugin fails to sanitize this input using functions like
sanitize_text_field()orsanitize_textarea_field()before callingupdate_post_meta(), malicious scripts can be stored in the database. - The Storage: The payload (e.g.,
<script>alert(1)</script>) is saved in the database as a string associated with a specific post and meta key. - The Sink (Output): When a user views the affected page or an admin dashboard listing, the plugin retrieves the data using
get_post_meta(). If the developer outputs this data directly—for example,echo $meta_value;—without using context-aware escaping functions likeesc_html()oresc_attr(), the browser interprets the string as active code, leading to XSS execution.
The Role of unfiltered_html
In standard WordPress installations, Administrators and Editors possess the unfiltered_html capability, which allows them to post raw HTML, including scripts. However, this capability is restricted in specific environments:
- Multisite Installations: Only Super Admins have
unfiltered_htmlby default. Regular site administrators cannot post raw HTML. - Hardened Environments: Security-conscious administrators may disable
unfiltered_htmlfor all users by addingdefine( 'DISALLOW_UNFILTERED_HTML', true );to thewp-config.phpfile.
A vulnerability is classified as an XSS issue when users without the unfiltered_html capability (or in environments where it is disabled) can still inject scripts through a field that lacks proper sanitization and escaping.
Defensive Remediation and Best Practices
To prevent Stored XSS in WordPress plugins, developers should adhere to the "Sanitize on Input, Escape on Output" principle:
1. Sanitize on Input
Always sanitize user input before it is saved to the database. This removes or neutralizes dangerous characters.
// Example of proper sanitization
if ( isset( $_POST['page_key'] ) ) {
update_post_meta( $post_id, 'page_key', sanitize_text_field( $_POST['page_key'] ) );
}
2. Verify Security Tokens (Nonces)
Ensure that any action that saves data is protected by a WordPress nonce to prevent Cross-Site Request Forgery (CSRF).
if ( ! isset( $_POST['my_nonce_field'] ) || ! wp_verify_nonce( $_POST['my_nonce_field'], 'save_page_key' ) ) {
return;
}
3. Escape on Output
Never trust data retrieved from the database, even if it was sanitized on input. Always escape it according to the context in which it will be rendered.
- HTML Body: Use
esc_html( $value ). - HTML Attributes: Use
esc_attr( $value ). - URLs: Use
esc_url( $value ). - JavaScript Variables: Use
wp_json_encode( $value ).
// Example of proper escaping
$page_key = get_post_meta( get_the_ID(), 'page_key', true );
echo '<div class="key-display">' . esc_html( $page_key ) . '</div>';
For more information on securing WordPress plugins, I recommend consulting the WordPress Plugin Handbook's security section.
Summary
The Page Keys plugin for WordPress (<= 1.3.3) is vulnerable to Stored Cross-Site Scripting via the 'page_key' parameter due to insufficient input sanitization and output escaping. This allows authenticated administrators to inject malicious scripts that execute in the context of other users' browsers, particularly in Multisite environments where 'unfiltered_html' is restricted.
Vulnerable Code
// In the metadata saving function (e.g., page-keys.php) if ( isset( $_POST['page_key'] ) ) { update_post_meta( $post_id, 'page_key', $_POST['page_key'] ); } --- // In the meta box rendering function (e.g., page-keys.php) $page_key = get_post_meta( $post->ID, 'page_key', true ); echo '<input type="text" name="page_key" value="' . $page_key . '">';
Security Fix
@@ -20,7 +20,7 @@ function save_page_key_meta( $post_id ) { if ( isset( $_POST['page_key'] ) ) { - update_post_meta( $post_id, 'page_key', $_POST['page_key'] ); + update_post_meta( $post_id, 'page_key', sanitize_text_field( $_POST['page_key'] ) ); } } @@ -35,5 +35,5 @@ function render_page_key_meta_box( $post ) { $page_key = get_post_meta( $post->ID, 'page_key', true ); - echo '<input type="text" name="page_key" value="' . $page_key . '">'; + echo '<input type="text" name="page_key" value="' . esc_attr( $page_key ) . '">'; }
Exploit Outline
The exploit requires Administrator-level privileges on a site where 'unfiltered_html' is disabled (such as a standard Multisite installation or a site with DISALLOW_UNFILTERED_HTML enabled). 1. An attacker navigates to the Page or Post editor screen where the Page Keys plugin adds its custom meta box. 2. The attacker enters a JavaScript payload (e.g., "><script>alert(1)</script>) into the 'page_key' input field. 3. The attacker saves the post, causing the payload to be stored in the 'wp_postmeta' table without sanitization. 4. The script executes whenever the meta box is rendered in the admin dashboard or if the 'page_key' is outputted on the frontend without proper escaping.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.