CVE-2026-3875

BetterDocs <= 4.3.8 - Authenticated (Contributor+) Stored Cross-Site Scripting via Shortcode Attributes

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

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: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<=4.3.8
PublishedApril 15, 2026
Last updatedApril 16, 2026
Affected pluginbetterdocs

What Changed in the Fix

Changes introduced in v4.3.9

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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.php using raw echo statements for label variables.
  • Preconditions: The plugin must be active. A Contributor user must be able to create or edit a post.

3. Code Flow

  1. Registration: WPDeveloper\BetterDocs\Core\ShortcodeFactory (referenced in includes/Plugin.php) registers the betterdocs_feedback_form shortcode.
  2. Processing: When a post containing the shortcode is rendered, the shortcode callback extracts attributes from the $atts array.
  3. Variable Assignment: The callback assigns attribute values (e.g., name_label) to local variables (e.g., $feedback_form_name_label_text).
  4. Template Loading: The plugin loads the template file views/shortcodes/feedback-form.php.
  5. The Sink: Inside views/shortcodes/feedback-form.php, the variables are output directly:
    // 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; ?>
    
    Unlike $button_text (line 27) which uses esc_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?.nonce or window.betterdocs_feedback_form_params?.nonce.
  • Acquisition:
    1. Create a page with the shortcode: wp post create --post_type=page --post_status=publish --post_content='[betterdocs_feedback_form]'
    2. Navigate to the page: browser_navigate(URL)
    3. Extract via JS: browser_eval("window.betterdocs_settings?.nonce")

5. Exploitation Strategy

The goal is to inject a script that executes when an Administrator views the page.

  1. Login as Contributor: Use the http_request tool to authenticate or use wp-cli to create a user and then authenticate.
  2. Inject Shortcode: Create a new post containing the malicious shortcode.
    • Payload: [betterdocs_feedback_form name_label='Name<script>alert(document.domain)</script>']
  3. Submit for Review: (Optional) As a Contributor, the post will be in pending status.
  4. Trigger (Admin Context): Use the browser_navigate tool logged in as an Administrator to view the post (either the public URL if published or the preview/edit URL if pending).
  5. Payload Variations:
    • Attribute breakout: name_label='"><script>alert(1)</script>'
    • Template context injection: message_label='</label><script>alert(1)</script><label>'

6. Test Data Setup

  1. Users:
    • admin_user (Administrator)
    • attacker_user (Contributor)
  2. Target Content:
    • A page or post created by attacker_user with 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")>']

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, or message_label attributes.
  • The raw HTML source of the rendered page will show the payload unescaped within the <label> tags of the betterdocs-feedback-form.

8. Verification Steps

  1. Check Post Content: wp post get <ID> --field=post_content to ensure the shortcode was saved correctly.
  2. Verify Frontend Output:
    • Navigate to the post URL.
    • Inspect the HTML: Search for form-name or form-email classes.
    • Confirm the presence of: <label for="message_name" class="form-name">Name<img src=x onerror=alert("XSS_NAME")> ...
  3. Confirm Execution: Use browser_eval to check for a global variable set by the payload (e.g., window.xss_executed = true).

9. Alternative Approaches

  • Attribute: button_text: Although line 27 uses esc_attr(), check if it's vulnerable to attribute breakout if unquoted or if esc_attr is 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.
Research Findings
Static analysis — not yet PoC-verified

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

--- /home/deploy/wp-safety.org/data/plugin-versions/betterdocs/4.3.8/views/shortcodes/feedback-form.php	2025-01-21 06:19:26.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/betterdocs/4.3.9/views/shortcodes/feedback-form.php	2026-03-11 10:19:56.000000000 +0000
@@ -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.