Unauthenticated Settings Overwrite in WooCommerce Multistore: CVE Research
Created September 10, 2026 • Last updated September 11, 2026 • 4 min read • 1,277 views
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 |
| All 13 handlers unlocked |
First subsite |
| All 13 handlers unlocked |
Large network |
| 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.
{"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:
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 |
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;
}