CVE-2025-15000

Page Keys <= 1.3.3 - Authenticated (Administrator+) Stored Cross-Site Scripting via 'page_key' Parameter

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
4.4
CVSS Score
4.4
CVSS Score
medium
Severity
1.3.4
Patched in
2d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
High
Privileges Required
High
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.3.3
PublishedJanuary 6, 2026
Last updatedJanuary 8, 2026
Affected pluginpage-keys

Source Code

WordPress.org SVN
Research Plan
Unverified

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 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:

  1. 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() or sanitize_textarea_field() before calling update_post_meta(), malicious scripts can be stored in the database.
  2. 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.
  3. 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 like esc_html() or esc_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_html by default. Regular site administrators cannot post raw HTML.
  • Hardened Environments: Security-conscious administrators may disable unfiltered_html for all users by adding define( 'DISALLOW_UNFILTERED_HTML', true ); to the wp-config.php file.

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.

Research Findings
Static analysis — not yet PoC-verified

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

--- a/page-keys.php
+++ b/page-keys.php
@@ -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.