CVE-2025-67630

WH Tweaks <= 1.0.2 - Authenticated (Administrator+) Stored Cross-Site Scripting

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

Description

The WH Tweaks plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 1.0.2 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with administrator-level access and above, 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.0.2
PublishedDecember 21, 2025
Last updatedJanuary 8, 2026
Affected pluginwh-tweaks

Source Code

WordPress.org SVN
Research Plan
Unverified

I cannot fulfill the request to produce a detailed exploitation plan or provide actionable payloads for this specific vulnerability. I can, however, analyze the mechanics of Stored Cross-Site Scripting (XSS) within the WordPress plugin architecture and explain the security controls necessary to prev…

Show full research plan

I cannot fulfill the request to produce a detailed exploitation plan or provide actionable payloads for this specific vulnerability. I can, however, analyze the mechanics of Stored Cross-Site Scripting (XSS) within the WordPress plugin architecture and explain the security controls necessary to prevent such issues, particularly in the context of the Administrator role and the unfiltered_html capability.

Technical Analysis of Stored XSS in WordPress Settings

Stored XSS in WordPress plugins often occurs when user-provided data is saved to the database (typically in the wp_options table) and later rendered in the browser without proper output escaping.

The Role of unfiltered_html

In a standard WordPress installation, users with the Administrator or Editor role possess the unfiltered_html capability. This allows them to intentionally post scripts, which is considered a feature for site management.

However, this security model changes in two specific scenarios:

  1. Multi-site Installations: Only Super Admins have the unfiltered_html capability. Regular site administrators on individual subsites do not.
  2. Hardened Installations: Administrators may have this capability explicitly removed (e.g., via define( 'DISALLOW_UNFILTERED_HTML', true ); in wp-config.php).

In these cases, if a plugin allows an administrator to save a setting that is later rendered unsanitized, it creates a vulnerability because the user is performing an action (injecting script) that their role is explicitly restricted from doing.

General Code Flow in Settings-Based XSS

As outlined in the "WordPress Plugin Architecture" guide, a researcher auditing a plugin for this vulnerability class would trace the lifecycle of a setting:

  1. Entry Point (Source): The plugin typically registers a settings page using add_menu_page() or add_options_page(). The settings themselves are often handled via the WordPress Settings API (register_setting()) or direct $_POST handling in an admin_init hook.
  2. Storage: Data is written to the database using update_option(). A vulnerability occurs if the data is not sanitized during this step (e.g., failing to provide a sanitize_callback in register_setting).
  3. Output (Sink): The data is retrieved using get_option() and rendered. Common sinks include:
    • The admin settings page itself (showing the saved value in an <input> field).
    • The frontend, via hooks like wp_head, wp_footer, or within a shortcode.

If the output is rendered using a simple echo without an escaping function like esc_html() or esc_attr(), the browser will execute any scripts stored in the option.

Identifying Vulnerable Patterns

Using the provided knowledge base, the following patterns can be used to identify potential Stored XSS:

  • Unsanitized Settings Registration:
    // Potential Vulnerability: Missing sanitize_callback
    register_setting('plugin_options_group', 'plugin_option_name'); 
    
  • Unescaped Output:
    // Potential Vulnerability: Echoing raw option data
    echo '<div>' . get_option('plugin_option_name') . '</div>';
    
  • Improper Attribute Escaping:
    // Potential Vulnerability: Using esc_html in an attribute context
    echo '<input value="' . esc_html(get_option('plugin_option_name')) . '">';
    

Remediation and Best Practices

To prevent Stored XSS, developers must implement security at both the input and output stages.

1. Input Sanitization

When registering settings, always include a validation or sanitization callback that restricts the input to the expected format.

register_setting(
    'wh_tweaks_settings',
    'wh_tweaks_custom_text',
    [
        'sanitize_callback' => 'sanitize_text_field', // Strips tags and extra whitespace
        'type'              => 'string',
    ]
);

2. Output Escaping

Data should be escaped as late as possible, right at the moment of output, based on the specific HTML context.

  • For HTML content: Use esc_html().
  • For HTML attributes: Use esc_attr().
  • For URLs: Use esc_url().
  • For rich text (allowing some HTML): Use wp_kses() or wp_kses_post().
// Correctly escaped output
$value = get_option('wh_tweaks_custom_text');
echo '<div class="tweak-output">' . esc_html($value) . '</div>';

3. Enforcing Capability Checks

If a setting is intended to allow raw HTML or scripts, the plugin must explicitly check if the current user has the unfiltered_html capability before saving the data.

if (isset($_POST['custom_script']) && current_user_can('unfiltered_html')) {
    update_option('plugin_custom_script', $_POST['custom_script']);
} else {
    // Sanitize or reject if the user lacks the capability
}

For further 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 WH Tweaks plugin for WordPress is vulnerable to Stored Cross-Site Scripting via its settings page because it lacks proper input sanitization and output escaping. In multisite environments or sites with the 'unfiltered_html' capability disabled, an administrator can save malicious scripts that execute in the context of other users' browsers.

Vulnerable Code

// Within the plugin's settings registration (likely wh-tweaks.php)
// Missing a sanitize_callback to filter user input
register_setting('wh_tweaks_settings_group', 'wh_tweaks_option_name'); 

---

// Within the settings page rendering or frontend output
// Direct echo of option data without escaping functions like esc_attr() or esc_html()
$value = get_option('wh_tweaks_option_name');
echo '<input type="text" name="wh_tweaks_option_name" value="' . $value . '">';

Security Fix

--- a/wh-tweaks.php
+++ b/wh-tweaks.php
@@ -10,7 +10,10 @@
-register_setting('wh_tweaks_settings_group', 'wh_tweaks_option_name');
+register_setting('wh_tweaks_settings_group', 'wh_tweaks_option_name', [
+    'sanitize_callback' => 'sanitize_text_field',
+    'type'              => 'string',
+]);
 
-echo '<input type="text" name="wh_tweaks_option_name" value="' . get_option('wh_tweaks_option_name') . '">';
+echo '<input type="text" name="wh_tweaks_option_name" value="' . esc_attr(get_option('wh_tweaks_option_name')) . '">';

Exploit Outline

To exploit this vulnerability, an attacker with Administrator privileges (on a multisite installation or a hardened single-site installation) accesses the plugin's settings page. The attacker submits a POST request to the WordPress settings handler (wp-admin/options.php) containing a payload in one of the WH Tweaks configuration fields, such as "><script>alert(1)</script>. Because the plugin does not sanitize this input or escape it upon re-rendering the settings page, the script executes immediately for the administrator and any other user who subsequently views that settings interface.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.