CVE-2026-2719

Private WP suite <= 0.4.1 - Authenticated (Administrator+) Stored Cross-Site Scripting via 'Exceptions' Setting

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
4.4
CVSS Score
4.4
CVSS Score
medium
Severity
Unpatched
Patched in
N/A
Time to patch

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: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<=0.4.1
PublishedApril 21, 2026
Last updatedApril 22, 2026
Affected pluginprivate-wp-suite
Research Plan
Unverified

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 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_html should ideally be disabled (e.g., via define( 'DISALLOW_UNFILTERED_HTML', true ); in wp-config.php), though the payload will work regardless of this setting if the code is unescaped.

3. Code Flow (Inferred)

  1. Entry (Admin UI): The administrator visits the plugin settings page (typically registered via add_options_page or add_menu_page in an admin class).
  2. Registration: The plugin uses register_setting( 'private_wp_suite_settings', 'private_wp_suite_options', ... ).
  3. Sink (Saving): When the form is submitted to options.php, WordPress calls the update_option logic. If the sanitize_callback is missing or insufficient (e.g., just trim), the XSS payload is stored in the wp_options table.
  4. Source (Rendering): The settings page callback (e.g., settings_page_output) retrieves the option using get_option( 'private_wp_suite_options' ).
  5. Execution: The value of the exceptions key is echoed inside a <textarea> or <div> without using esc_textarea() or esc_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.

  1. Identify Page: The settings page is likely at /wp-admin/options-general.php?page=private-wp-suite.
  2. Navigate: Use browser_navigate to reach this page.
  3. Extract Nonce: Use browser_eval to extract the _wpnonce and option_page fields from the HTML form.
    • option_page: The value of input[name="option_page"].
    • _wpnonce: The value of input[name="_wpnonce"].
  4. 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

  1. Preparation: Log in as Administrator.
  2. 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.
  3. 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>
        
  4. Triggering: Navigate back to the settings page. The script should execute immediately.

6. Test Data Setup

  1. Plugin Installation: Install and activate private-wp-suite version 0.4.1.
  2. Environment Check: Ensure the user has the Administrator role.
  3. (Optional) Harden Environment: Add define( 'DISALLOW_UNFILTERED_HTML', true ); to wp-config.php to 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=true parameter.
  • 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

  1. CLI Check: Verify the option value in the database:
    wp option get private_wp_suite_options
    Check if the exceptions key contains the <script> payload.
  2. Browser Verification: Use browser_navigate to the settings page and check for the presence of the script in the DOM or use browser_eval to 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 exceptions is 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 input value attribute instead of a textarea, use: " onmouseover="alert(1)" type="text.
Research Findings
Static analysis — not yet PoC-verified

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

--- a/private-wp-suite/admin/settings.php
+++ b/private-wp-suite/admin/settings.php
@@ -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.