WH Tweaks <= 1.0.2 - Authenticated (Administrator+) Stored Cross-Site Scripting
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:NTechnical Details
<=1.0.2Source Code
WordPress.org SVNI 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:
- Multi-site Installations: Only Super Admins have the
unfiltered_htmlcapability. Regular site administrators on individual subsites do not. - Hardened Installations: Administrators may have this capability explicitly removed (e.g., via
define( 'DISALLOW_UNFILTERED_HTML', true );inwp-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:
- Entry Point (Source): The plugin typically registers a settings page using
add_menu_page()oradd_options_page(). The settings themselves are often handled via the WordPress Settings API (register_setting()) or direct$_POSThandling in anadmin_inithook. - 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 asanitize_callbackinregister_setting). - 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.
- The admin settings page itself (showing the saved value in an
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()orwp_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.
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
@@ -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.