
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
CVE-2026-12731
wedocs
2.3.0
2.3.1
Analysis Overview
Differential between vulnerable and patched code
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- 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
What is CVE-2026-12731?
Vulnerability overviewCVE-2026-12731 is a stored cross-site scripting (XSS) vulnerability in the weDocs: AI Powered Knowledge Base plugin for WordPress. It affects versions up to and including 2.3.0 and allows authenticated attackers with contributor-level access or higher to inject arbitrary web scripts via the ‘sectionTitleTag’ and ‘articleTitleTag’ block attributes.
How does the vulnerability work?
Root cause and exploitationThe vulnerability exists because the plugin does not properly sanitize or escape the ‘sectionTitleTag’ and ‘articleTitleTag’ attributes in the Sidebar block. An attacker can set these attributes to malicious values like ‘h3 onclick=alert(1)’, which are then output directly into HTML tags without validation, allowing script execution when the page is viewed.
Who is affected by this vulnerability?
Affected users and versionsAny WordPress site running weDocs version 2.3.0 or earlier is affected. The vulnerability can be exploited by any authenticated user with contributor-level access or higher, meaning users who can create or edit posts using the block editor.
What is the CVSS score and severity?
Risk assessmentThe vulnerability has a CVSS score of 6.4, which is classified as Medium severity. This indicates a moderate risk: exploitation requires authentication and user interaction, but the impact includes potential session hijacking, data theft, or further compromise if an administrator views the injected page.
How can I check if my site is vulnerable?
Detection methodsCheck the weDocs plugin version in your WordPress admin dashboard under Plugins. If the version is 2.3.0 or lower, your site is vulnerable. You can also review the render.php file in the plugin for the absence of sanitization functions like ‘wedocs_sanitize_tag_name’ and ‘wedocs_sanitize_css_length’.
How do I fix the vulnerability?
Patch and updateUpdate the weDocs plugin to version 2.3.1 or later, which introduces proper sanitization functions for tag names and CSS lengths. The patch validates tag names against a whitelist and CSS lengths against a regex pattern, preventing injection of malicious code.
What if I cannot update the plugin immediately?
Temporary mitigationsAs a temporary measure, restrict contributor-level access to trusted users only, or disable the weDocs Sidebar block by removing or modifying the block registration. You can also use a web application firewall (WAF) with rules that block malicious input patterns targeting the vulnerable attributes.
What does the proof of concept demonstrate?
PoC explanationThe proof of concept shows how an attacker with contributor credentials can create a post containing a weDocs Sidebar block with a malicious ‘sectionTitleTag’ attribute. When the post is published and viewed, the injected JavaScript executes, demonstrating stored XSS. The PoC uses PHP to automate login and post creation.
What is the impact of stored XSS?
Practical consequencesStored XSS allows an attacker to execute arbitrary JavaScript in the browser of any user who views the affected page. This can lead to session cookie theft, redirection to malicious sites, defacement, or performing actions on behalf of the victim, such as creating new admin accounts.
Why is contributor-level access a concern?
Attack surfaceContributor-level users typically have limited privileges but can create and edit posts. This vulnerability allows them to escalate their impact by injecting scripts that execute in the context of higher-privileged users, potentially leading to full site compromise if an administrator views the injected content.
How does the patch prevent XSS?
Patch analysisThe patch adds two sanitization functions: ‘wedocs_sanitize_tag_name’ restricts tag names to a whitelist of safe HTML tags, and ‘wedocs_sanitize_css_length’ validates CSS length values against a strict pattern. These functions are applied to the vulnerable attributes before output, ensuring only safe values are used.
What should I do if I suspect exploitation?
Incident responseImmediately update the plugin to version 2.3.1. Review recent posts created by contributor-level users for suspicious block attributes. Check your site’s database for injected scripts in post content. Change all user passwords and consider rotating session keys. Monitor logs for unusual activity.
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.
Trusted by Developers & Organizations






