BetterDocs <= 4.3.8 - Authenticated (Contributor+) Stored Cross-Site Scripting via Shortcode Attributes
Description
The BetterDocs plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'betterdocs_feedback_form' shortcode in all versions up to, and including, 4.3.8. This is due to insufficient input sanitization and output escaping on user supplied shortcode attributes. This makes it possible for authenticated attackers, with contributor 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 v4.3.9
Source Code
WordPress.org SVN# Vulnerability Analysis: CVE-2026-3875 - BetterDocs Stored XSS via Shortcode Attributes ## 1. Vulnerability Summary The **BetterDocs** plugin (<= 4.3.8) is vulnerable to **Authenticated Stored Cross-Site Scripting (XSS)**. The flaw exists in the handling of the `betterdocs_feedback_form` shortcode…
Show full research plan
Vulnerability Analysis: CVE-2026-3875 - BetterDocs Stored XSS via Shortcode Attributes
1. Vulnerability Summary
The BetterDocs plugin (<= 4.3.8) is vulnerable to Authenticated Stored Cross-Site Scripting (XSS). The flaw exists in the handling of the betterdocs_feedback_form shortcode. Specifically, several attributes used to define labels within the feedback form are rendered in the frontend template without undergoing proper sanitization (on input) or escaping (on output). This allows a user with Contributor level permissions or higher to embed malicious JavaScript within a post or page.
2. Attack Vector Analysis
- Shortcode:
[betterdocs_feedback_form] - Vulnerable Attributes (Inferred):
name_label,email_label,message_label(mapping to$feedback_form_name_label_text, etc. in the template). - Authentication Level: Contributor+ (any role allowed to use shortcodes in posts).
- Sink:
views/shortcodes/feedback-form.phpusing rawechostatements for label variables. - Preconditions: The plugin must be active. A Contributor user must be able to create or edit a post.
3. Code Flow
- Registration:
WPDeveloper\BetterDocs\Core\ShortcodeFactory(referenced inincludes/Plugin.php) registers thebetterdocs_feedback_formshortcode. - Processing: When a post containing the shortcode is rendered, the shortcode callback extracts attributes from the
$attsarray. - Variable Assignment: The callback assigns attribute values (e.g.,
name_label) to local variables (e.g.,$feedback_form_name_label_text). - Template Loading: The plugin loads the template file
views/shortcodes/feedback-form.php. - The Sink: Inside
views/shortcodes/feedback-form.php, the variables are output directly:
Unlike// views/shortcodes/feedback-form.php lines 6, 12, 18 <?php echo $feedback_form_name_label_text; ?> <?php echo $feedback_form_email_label_text; ?> <?php echo $feedback_form_message_label_text; ?>$button_text(line 27) which usesesc_attr(), these labels are not escaped, leading to XSS.
4. Nonce Acquisition Strategy
For this specific Stored XSS via Shortcode, a WordPress nonce is not required to trigger the vulnerability itself, as the exploitation happens during the standard post-creation process (Contributor) and the subsequent page rendering (Victim).
However, if the agent needs to interact with the feedback form's AJAX submission (to test if the XSS can be triggered via form response), a nonce for the action betterdocs_feedback_form_action (or similar) might be needed.
- Location: Nonces are typically localized in
includes/Core/Scripts.php. - JS Variable: Likely
window.betterdocs_settings?.nonceorwindow.betterdocs_feedback_form_params?.nonce. - Acquisition:
- Create a page with the shortcode:
wp post create --post_type=page --post_status=publish --post_content='[betterdocs_feedback_form]' - Navigate to the page:
browser_navigate(URL) - Extract via JS:
browser_eval("window.betterdocs_settings?.nonce")
- Create a page with the shortcode:
5. Exploitation Strategy
The goal is to inject a script that executes when an Administrator views the page.
- Login as Contributor: Use the
http_requesttool to authenticate or usewp-clito create a user and then authenticate. - Inject Shortcode: Create a new post containing the malicious shortcode.
- Payload:
[betterdocs_feedback_form name_label='Name<script>alert(document.domain)</script>']
- Payload:
- Submit for Review: (Optional) As a Contributor, the post will be in
pendingstatus. - Trigger (Admin Context): Use the
browser_navigatetool logged in as an Administrator to view the post (either the public URL if published or the preview/edit URL if pending). - Payload Variations:
- Attribute breakout:
name_label='"><script>alert(1)</script>' - Template context injection:
message_label='</label><script>alert(1)</script><label>'
- Attribute breakout:
6. Test Data Setup
- Users:
admin_user(Administrator)attacker_user(Contributor)
- Target Content:
- A page or post created by
attacker_userwith the content:[betterdocs_feedback_form name_label='Name<img src=x onerror=alert("XSS_NAME")>' email_label='Email<img src=x onerror=alert("XSS_EMAIL")>' message_label='Msg<img src=x onerror=alert("XSS_MSG")>']
- A page or post created by
7. Expected Results
- When the Administrator navigates to the post containing the shortcode, the browser should execute the JavaScript contained in the
name_label,email_label, ormessage_labelattributes. - The raw HTML source of the rendered page will show the payload unescaped within the
<label>tags of thebetterdocs-feedback-form.
8. Verification Steps
- Check Post Content:
wp post get <ID> --field=post_contentto ensure the shortcode was saved correctly. - Verify Frontend Output:
- Navigate to the post URL.
- Inspect the HTML: Search for
form-nameorform-emailclasses. - Confirm the presence of:
<label for="message_name" class="form-name">Name<img src=x onerror=alert("XSS_NAME")> ...
- Confirm Execution: Use
browser_evalto check for a global variable set by the payload (e.g.,window.xss_executed = true).
9. Alternative Approaches
- Attribute:
button_text: Although line 27 usesesc_attr(), check if it's vulnerable to attribute breakout if unquoted or ifesc_attris bypassed via double encoding (unlikely in this version). - Shortcode:
betterdocs_search_form: If the feedback form is patched or unavailable, other BetterDocs shortcodes often share similar logic for labels and placeholders. - DOM XSS: Check
assets/js/betterdocs-feedback-form.js(if it exists) to see if it handles the submission response by injecting it into.response(line 2 of the template) using.innerHTML. If so, the XSS could be triggered via the feedback submission response if the server-side handler is also unescaped.
Summary
The BetterDocs plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'betterdocs_feedback_form' shortcode attributes in versions up to 4.3.8. This allows authenticated attackers with Contributor-level permissions or higher to inject arbitrary JavaScript into a post or page, which executes in the context of any user viewing the page.
Vulnerable Code
// views/shortcodes/feedback-form.php lines 6, 12, 18 <label for="message_name" class="form-name"> <?php echo $feedback_form_name_label_text; ?> <span>*</span> <br> <input type="text" id="message_name" name="message_name" aria-label="<?php echo esc_html( 'Name', 'betterdocs' ); ?>" value="<?php echo esc_html( $name ); ?>" /> </label> --- <label for="message_email" class="form-email"> <?php echo $feedback_form_email_label_text; ?> <span>*</span> <br> <input type="text" id="message_email" name="message_email" aria-label="<?php echo esc_html( 'Email', 'betterdocs' ); ?>" value="<?php echo esc_html( $email ); ?>" /> </label> --- <label for="message_text" class="form-message"> <?php echo $feedback_form_message_label_text; ?> <span>*</span> <br> <textarea type="text" id="message_text" aria-label="<?php echo esc_html( 'Message', 'betterdocs' ); ?>" name="message_text"></textarea> </label>
Security Fix
@@ -3,19 +3,19 @@ <form id="betterdocs-feedback-form" class="betterdocs-feedback-form" action="" method="post"> <p> <label for="message_name" class="form-name"> - <?php echo $feedback_form_name_label_text; ?> <span>*</span> <br> - <input type="text" id="message_name" name="message_name" aria-label="<?php echo esc_html( 'Name', 'betterdocs' ); ?>" value="<?php echo esc_html( $name ); ?>" /> + <?php echo esc_html( $feedback_form_name_label_text ); ?> <span>*</span> <br> + <input type="text" id="message_name" name="message_name" aria-label="<?php echo esc_html( 'Name', 'betterdocs' ); ?>" value="<?php echo esc_attr( $name ); ?>" /> </label> </p> <p> <label for="message_email" class="form-email"> - <?php echo $feedback_form_email_label_text; ?> <span>*</span> <br> - <input type="text" id="message_email" name="message_email" aria-label="<?php echo esc_html( 'Email', 'betterdocs' ); ?>" value="<?php echo esc_html( $email ); ?>" /> + <?php echo esc_html( $feedback_form_email_label_text ); ?> <span>*</span> <br> + <input type="text" id="message_email" name="message_email" aria-label="<?php echo esc_html( 'Email', 'betterdocs' ); ?>" value="<?php echo esc_attr( $email ); ?>" /> </label> </p> <p> <label for="message_text" class="form-message"> - <?php echo $feedback_form_message_label_text; ?> <span>*</span> <br> + <?php echo esc_html( $feedback_form_message_label_text ); ?> <span>*</span> <br> <textarea type="text" id="message_text" aria-label="<?php echo esc_html( 'Message', 'betterdocs' ); ?>" name="message_text"></textarea> </label> </p>
Exploit Outline
1. Authenticate as a user with Contributor-level privileges or higher. 2. Create a new post or edit an existing one. 3. Embed the following shortcode into the post content: [betterdocs_feedback_form name_label="Name<script>alert(document.domain)</script>"]. 4. Save the post (and submit for review if necessary). 5. As a victim (e.g., an Administrator), navigate to the page where the post is rendered. 6. The payload will execute because the `name_label` attribute is echoed directly into the HTML without sanitization or escaping.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.