Private WP suite <= 0.4.1 - Authenticated (Administrator+) Stored Cross-Site Scripting via 'Exceptions' Setting
Description
The Private WP suite plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'Exceptions' setting in all versions up to, and including, 0.4.1. This is 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
<=0.4.1This research plan outlines the steps required to demonstrate Stored Cross-Site Scripting (XSS) in the Private WP suite plugin via the 'Exceptions' setting. ### 1. Vulnerability Summary The **Private WP suite** plugin (<= 0.4.1) allows administrators to define "Exceptions"—specific URLs or pages th…
Show full research plan
This research plan outlines the steps required to demonstrate Stored Cross-Site Scripting (XSS) in the Private WP suite plugin via the 'Exceptions' setting.
1. Vulnerability Summary
The Private WP suite plugin (<= 0.4.1) allows administrators to define "Exceptions"—specific URLs or pages that remain public even when the rest of the site is restricted. The plugin fails to sanitize this input when saving it to the database and fails to escape it when rendering it back into the admin settings page (and potentially on the frontend). In environments where unfiltered_html is disabled (like WordPress Multisite), this allows an administrator to inject malicious JavaScript that will execute in the context of any user (including Super Admins) who visits the settings page.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/options.php(Standard WordPress Settings API endpoint). - Vulnerable Parameter:
private_wp_suite_options[exceptions](inferred based on standard plugin naming conventions). - Authentication: Requires Administrator-level privileges.
- Preconditions: The plugin must be active. To demonstrate the security impact,
unfiltered_htmlshould ideally be disabled (e.g., viadefine( 'DISALLOW_UNFILTERED_HTML', true );inwp-config.php), though the payload will work regardless of this setting if the code is unescaped.
3. Code Flow (Inferred)
- Entry (Admin UI): The administrator visits the plugin settings page (typically registered via
add_options_pageoradd_menu_pagein an admin class). - Registration: The plugin uses
register_setting( 'private_wp_suite_settings', 'private_wp_suite_options', ... ). - Sink (Saving): When the form is submitted to
options.php, WordPress calls theupdate_optionlogic. If thesanitize_callbackis missing or insufficient (e.g., justtrim), the XSS payload is stored in thewp_optionstable. - Source (Rendering): The settings page callback (e.g.,
settings_page_output) retrieves the option usingget_option( 'private_wp_suite_options' ). - Execution: The value of the
exceptionskey is echoed inside a<textarea>or<div>without usingesc_textarea()oresc_html(). An attacker can close the tag (e.g.,</textarea>) and inject a<script>tag.
4. Nonce Acquisition Strategy
The Settings API protects form submissions using a nonce.
- Identify Page: The settings page is likely at
/wp-admin/options-general.php?page=private-wp-suite. - Navigate: Use
browser_navigateto reach this page. - Extract Nonce: Use
browser_evalto extract the_wpnonceandoption_pagefields from the HTML form.option_page: The value ofinput[name="option_page"]._wpnonce: The value ofinput[name="_wpnonce"].
- JavaScript Extraction:
(() => { return { nonce: document.querySelector('input[name="_wpnonce"]')?.value, option_page: document.querySelector('input[name="option_page"]')?.value, settings_field_name: document.querySelector('textarea[name*="exceptions"]')?.name }; })()
5. Exploitation Strategy
- Preparation: Log in as Administrator.
- Information Gathering:
- Navigate to the Private WP suite settings page.
- Identify the exact name of the textarea for "Exceptions". It is likely
private_wp_suite_options[exceptions]. - Extract the
_wpnonce.
- Execution (Payload Injection):
- Send a POST request to
/wp-admin/options.php. - Payload:
</textarea><script>alert(document.domain + " - XSS")</script> - Request Details:
- Method: POST
- URL:
http://localhost:8080/wp-admin/options.php - Content-Type:
application/x-www-form-urlencoded - Body:
option_page=[EXTRACTED_OPTION_PAGE]& action=update& _wpnonce=[EXTRACTED_NONCE]& private_wp_suite_options[exceptions]=</textarea><script>alert(window.origin)</script>
- Send a POST request to
- Triggering: Navigate back to the settings page. The script should execute immediately.
6. Test Data Setup
- Plugin Installation: Install and activate
private-wp-suiteversion 0.4.1. - Environment Check: Ensure the user has the Administrator role.
- (Optional) Harden Environment: Add
define( 'DISALLOW_UNFILTERED_HTML', true );towp-config.phpto prove the bypass of intended WordPress security restrictions.
7. Expected Results
- Upon submitting the POST request, the server should return a 302 redirect back to the settings page with a
settings-updated=trueparameter. - When the browser loads the settings page, an alert box showing the origin/domain should appear.
- The HTML source of the page will show the payload rendered raw:
<textarea ...></textarea><script>alert(window.origin)</script></textarea>
8. Verification Steps
- CLI Check: Verify the option value in the database:
wp option get private_wp_suite_options
Check if theexceptionskey contains the<script>payload. - Browser Verification: Use
browser_navigateto the settings page and check for the presence of the script in the DOM or usebrowser_evalto check if a global variable set by the script exists.
9. Alternative Approaches
- Action-based XSS: If the "Exceptions" are used on the frontend to determine access, check if the payload triggers for unauthenticated visitors when they access the site.
- Bypass through different fields: If
exceptionsis sanitized, check other settings fields like "Login Page Message" or custom redirect URLs, which often suffer from the same lack of escaping. - XSS via Attribute Injection: If the payload is rendered inside an
inputvalue attribute instead of atextarea, use:" onmouseover="alert(1)" type="text.
Summary
The Private WP suite plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'Exceptions' setting in versions up to 0.4.1. This vulnerability allows authenticated administrators to inject malicious scripts into the plugin's configuration, which then execute when any user (including Super Admins) visits the settings page. This is particularly impactful in Multisite environments where the 'unfiltered_html' capability is typically restricted for site administrators.
Vulnerable Code
// Inferred registration of settings without a sanitization callback // Likely in an admin initialization function register_setting( 'private_wp_suite_settings', 'private_wp_suite_options' ); --- // Inferred rendering logic in the admin settings page callback // The stored option is echoed directly without using esc_textarea() or esc_html() $options = get_option( 'private_wp_suite_options' ); $exceptions = isset( $options['exceptions'] ) ? $options['exceptions'] : ''; ?> <textarea name="private_wp_suite_options[exceptions]" rows="10" cols="50"> <?php echo $exceptions; ?> </textarea> <?php
Security Fix
@@ -10,7 +10,13 @@ -register_setting( 'private_wp_suite_settings', 'private_wp_suite_options' ); +register_setting( 'private_wp_suite_settings', 'private_wp_suite_options', array( + 'sanitize_callback' => 'private_wp_suite_sanitize_options' +) ); + +function private_wp_suite_sanitize_options( $input ) { + if ( isset( $input['exceptions'] ) ) { + $input['exceptions'] = sanitize_textarea_field( $input['exceptions'] ); + } + return $input; +} @@ -45,5 +51,5 @@ <textarea name="private_wp_suite_options[exceptions]" rows="10" cols="50"> - <?php echo $options['exceptions']; ?> + <?php echo esc_textarea( $options['exceptions'] ); ?> </textarea>
Exploit Outline
The exploit targets the WordPress Settings API to store a malicious payload in the plugin's configuration. 1. **Authentication**: The attacker must be logged in with Administrator-level privileges. 2. **Target Identification**: Navigate to the Private WP suite settings page (usually under Settings -> Private WP Suite) to identify the option group name and the specific name of the 'Exceptions' textarea. 3. **Nonce Acquisition**: Extract the `_wpnonce` and `option_page` values from the HTML form on the settings page. 4. **Payload Injection**: Send a POST request to `/wp-admin/options.php` with the following parameters: - `option_page`: The extracted settings group name. - `_wpnonce`: The extracted CSRF nonce. - `action`: `update` - `private_wp_suite_options[exceptions]`: `</textarea><script>alert(document.domain)</script>` 5. **Execution**: The payload is stored in the `wp_options` table. The XSS triggers automatically when any administrative user revisits the plugin settings page, as the payload breaks out of the `<textarea>` context and executes the injected `<script>` tag.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.