CVE-2025-13921

weDocs <= 2.1.16 - Missing Authorization to Authenticated (Subscriber+) Documentation Post Update

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
2.1.17
Patched in
2d
Time to patch

Description

The weDocs: AI Powered Knowledge Base, Docs, Documentation, Wiki & AI Chatbot plugin for WordPress is vulnerable to unauthorized modification or loss of data due to a missing capability check on the 'wedocs_user_documentation_handling_capabilities' function in all versions up to, and including, 2.1.16. This makes it possible for authenticated attackers, with Subscriber-level access and above, to edit any documentation post. The vulnerability was partially patched in version 2.1.16.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=2.1.16
PublishedJanuary 22, 2026
Last updatedJanuary 23, 2026
Affected pluginwedocs

What Changed in the Fix

Changes introduced in v2.1.17

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2025-13921 weDocs Missing Authorization ## 1. Vulnerability Summary The **weDocs** plugin for WordPress (up to version 2.1.16) contains an authorization vulnerability where documentation-specific capabilities are incorrectly granted to all user roles, including the…

Show full research plan

Exploitation Research Plan: CVE-2025-13921 weDocs Missing Authorization

1. Vulnerability Summary

The weDocs plugin for WordPress (up to version 2.1.16) contains an authorization vulnerability where documentation-specific capabilities are incorrectly granted to all user roles, including the Subscriber role. This occurs in the add_documentation_handling_capabilities function within the upgrade handler. Specifically, the plugin grants edit_others_docs and edit_published_docs to every registered role, effectively allowing any authenticated user to modify documentation posts created by others.

2. Attack Vector Analysis

  • Endpoint: WordPress REST API (/wp-json/wp/v2/docs/<id>) or standard AJAX handlers.
  • Payload Parameter: content, title, or any other post field.
  • Authentication: Required (Subscriber-level or higher).
  • Preconditions: The upgrade logic (specifically the code in includes/Upgrader/Upgrades/V_2_0_2.php) must have executed, which typically happens automatically upon installation or update of the plugin.

3. Code Flow

  1. Capability Assignment:
    • File: includes/Upgrader/Upgrades/V_2_0_2.php
    • Function: add_documentation_handling_capabilities()
    • Logic:
      $roles = $wp_roles->get_names(); // Retrieves ALL roles (subscriber, contributor, etc.)
      $capabilities = array(
          'edit_post',
          'edit_docs',
          'publish_docs',
          'edit_others_docs',
          'read_private_docs',
          'edit_private_docs',
          'edit_published_docs'
      );
      foreach ( $capabilities as $capability ) {
          foreach ( $roles as $role_key => $role ) {
              $wp_roles->add_cap( $role_key, $capability ); // BUG: Grants caps to ALL roles
          }
      }
      
  2. Accessing the Sink:
    • WordPress registers the docs post type (likely in includes/Post_Types.php, though file content is not provided).
    • When a user attempts to edit a docs post via the REST API or Admin dashboard, WordPress core checks current_user_can( 'edit_post', $post_id ).
    • Because the Subscriber now has edit_others_docs, this check passes for any documentation post.

4. Nonce Acquisition Strategy

Since the attacker is an authenticated Subscriber, they can log into the WordPress dashboard (/wp-admin/).

  1. Identify Script Variable: WordPress naturally exposes the REST API nonce to authenticated users in the wp-admin dashboard via the wpApiSettings object.
  2. Strategy:
    • Log in as the Subscriber.
    • Navigate to /wp-admin/profile.php.
    • Use browser_eval to extract the nonce:
      browser_eval("window.wpApiSettings?.nonce")
  3. Alternative (if REST is disabled): Look for the wedocs localized JS object for AJAX nonces, often found on documentation pages.

5. Exploitation Strategy

Step 1: Confirm Capability Grant

The upgrade code in V_2_0_2.php runs if the wedocs_version option in the database is less than 2.0.2. To trigger it in a test environment, the version might need to be spoofed or the plugin simply activated.

Step 2: Identify Target Doc

Determine the ID of a docs post created by an Administrator.

Step 3: Perform Unauthorized Update

Use the Subscriber's session and the REST nonce to update the post.

HTTP Request:

POST /wp-json/wp/v2/docs/[DOC_ID] HTTP/1.1
Host: [TARGET_HOST]
Content-Type: application/json
X-WP-Nonce: [EXTRACTED_NONCE]
Cookie: [SUBSCRIBER_COOKIES]

{
  "content": "<h1>Hacked by Subscriber</h1>",
  "title": "Unauthorized Change"
}

6. Test Data Setup

  1. Install weDocs 2.1.16.
  2. Create Content: As an Admin, create a new Documentation post (post type docs) and publish it. Note its ID (e.g., 123).
  3. Create Attacker: Create a user with the Subscriber role.
  4. Trigger Upgrade: Visit the Admin dashboard once as Admin to ensure UpgradeHandler::check() runs and invokes V_2_0_2::handle_upgrade().

7. Expected Results

  • The REST API should return a 200 OK (or 201 Created) response.
  • The response body should contain the updated content: "content": {"rendered": "<h1>Hacked by Subscriber</h1>", ...}.
  • The Subscriber, who normally lacks permissions to edit posts (especially others' posts), successfully bypasses authorization.

8. Verification Steps

  1. Check Capabilities via CLI:
    wp user cap list [SUBSCRIBER_ID]
    Verify edit_others_docs and edit_published_docs appear in the list.
  2. Verify Post Content:
    wp post get [DOC_ID] --field=post_content
    Confirm the content matches the payload sent by the Subscriber.

9. Alternative Approaches

  • Standard AJAX: If the REST API is restricted, search for AJAX actions. Based on vendor/composer/autoload_classmap.php, WeDevs\WeDocs\Ajax exists. Grep for wp_ajax_ in the plugin directory to find documentation saving handlers (e.g., wedocs_save_doc_data).
  • Classic Editor: Attempt to access wp-admin/post.php?post=[DOC_ID]&action=edit as the Subscriber. If the capabilities were granted, the WordPress UI will allow the Subscriber to view the editor for that post.
Research Findings
Static analysis — not yet PoC-verified

Summary

The weDocs plugin incorrectly grants documentation-specific capabilities, such as edit_others_docs and edit_published_docs, to all registered WordPress roles during its version 2.0.2 upgrade process. This allows authenticated users with low-level privileges, like Subscribers, to modify or delete any documentation post via the WordPress REST API or Admin dashboard.

Vulnerable Code

// includes/Upgrader/Upgrades/V_2_0_2.php

    /**
     * Add weDocs documentation handling capabilities for users.
     *
     * @since 2.0.2
     *
     * @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 );
            }
        }
    }

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wedocs/2.1.16/includes/Upgrader/Abstracts/UpgradeHandler.php /home/deploy/wp-safety.org/data/plugin-versions/wedocs/2.1.17/includes/Upgrader/Abstracts/UpgradeHandler.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wedocs/2.1.16/includes/Upgrader/Abstracts/UpgradeHandler.php	2023-11-06 10:04:00.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wedocs/2.1.17/includes/Upgrader/Abstracts/UpgradeHandler.php	2026-01-15 07:05:56.000000000 +0000
@@ -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();
     }
 
     /**

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wedocs/2.1.16/includes/Upgrader/Upgrades/Upgrades.php /home/deploy/wp-safety.org/data/plugin-versions/wedocs/2.1.17/includes/Upgrader/Upgrades/Upgrades.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wedocs/2.1.16/includes/Upgrader/Upgrades/Upgrades.php	2024-01-02 10:57:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wedocs/2.1.17/includes/Upgrader/Upgrades/Upgrades.php	2026-01-15 07:05:56.000000000 +0000
@@ -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.
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wedocs/2.1.16/includes/Upgrader/Upgrades/V_2_0_2.php /home/deploy/wp-safety.org/data/plugin-versions/wedocs/2.1.17/includes/Upgrader/Upgrades/V_2_0_2.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wedocs/2.1.16/includes/Upgrader/Upgrades/V_2_0_2.php	2024-01-02 10:57:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wedocs/2.1.17/includes/Upgrader/Upgrades/V_2_0_2.php	2026-01-15 07:05:56.000000000 +0000
@@ -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();
     }
 }

Exploit Outline

The exploit methodology involves leveraging the erroneously granted capabilities to a Subscriber-level account. 1. Authentication: The attacker must log into the WordPress site as a user with the Subscriber role. 2. Nonce Acquisition: Once logged in, the attacker can find a valid REST API nonce in the page source of the WordPress dashboard (e.g., via the `wpApiSettings` JavaScript object). 3. Target Identification: The attacker identifies the ID of a documentation post (post type `docs`) that they wish to modify. 4. Endpoint Hit: The attacker sends an authenticated POST request to the WordPress REST API endpoint `/wp-json/wp/v2/docs/<ID>`. 5. Payload: The request body includes updated post content or titles. Because the Subscriber's role contains the `edit_others_docs` and `edit_published_docs` capabilities (granted by the vulnerable upgrade handler), WordPress core permits the update despite the user normally lacking such permissions.

Check if your site is affected.

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