Order Splitter for WooCommerce <= 5.3.5 - Missing Authorization to Authenticated (Subscriber+) Order Information Exposure
Description
The Order Splitter for WooCommerce plugin for WordPress is vulnerable to unauthorized access of data due to a missing capability check on the 'wos_troubleshooting' AJAX endpoint in all versions up to, and including, 5.3.5. This makes it possible for authenticated attackers, with Subscriber-level access and above, to view information pertaining to other user's orders.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:NTechnical Details
<=5.3.5What Changed in the Fix
Changes introduced in v5.3.6
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2025-12075 ## 1. Vulnerability Summary The **Order Splitter for WooCommerce** plugin (versions <= 5.3.5) contains a missing authorization vulnerability within its troubleshooting functionality. Specifically, the AJAX action `wos_troubleshooting` registered in `inc/…
Show full research plan
Exploitation Research Plan: CVE-2025-12075
1. Vulnerability Summary
The Order Splitter for WooCommerce plugin (versions <= 5.3.5) contains a missing authorization vulnerability within its troubleshooting functionality. Specifically, the AJAX action wos_troubleshooting registered in inc/functions-troubleshooting.php fails to perform any capability checks (e.g., current_user_can( 'manage_options' )) or verify that the requested order belongs to the authenticated user. This allows any logged-in user, including those with Subscriber-level permissions, to access sensitive order data, metadata, and plugin configuration by simply providing a target order_id.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
wos_troubleshooting - HTTP Method:
POST - Vulnerable Parameter:
order_id - Authentication: Required (Subscriber-level or higher).
- Preconditions: At least one WooCommerce order must exist in the system for information to be exposed. The plugin must be active.
3. Code Flow
- Entry Point: The plugin registers the AJAX handler in
inc/functions-troubleshooting.phpusing:add_action( 'wp_ajax_wos_troubleshooting', 'wos_troubleshooting' ); - Input Processing: In the
wos_troubleshooting()function, the code executesextract($_POST);. This insecurely populates the local scope with variables from the POST request, including$order_id. - Information Retrieval:
- The code calls
$order = wc_get_order($order_id);without checking if the current user has theedit_shop_ordercapability or is the owner of the order. - It retrieves order metadata via
wc_os_get_order_meta($order_id). - It iterates through order items using
$order->get_items(), retrievingproduct_id,vendor_id,vendor_name, anditem_meta. - It performs complex logic via
wc_os_order_splitter->split_order_logic($order_id, false, true).
- The code calls
- Data Sink: The accumulated information is stored in the
$debug_outputarray, which is then imploded into$ret['html']. - Response: The function terminates with
wp_send_json($ret);, returning the complete HTML-formatted debug information (including PII from Order Meta) to the requester.
4. Nonce Acquisition Strategy
Based on the provided source code for wos_troubleshooting() in inc/functions-troubleshooting.php, there is no nonce verification (i.e., no check_ajax_referer or wp_verify_nonce) present in the function.
If a generic nonce were required by the WordPress AJAX framework or added in a global plugin initialization, a Subscriber could obtain it by:
- Creating a post/page with a plugin shortcode (if one exists).
- Navigating to the page and using
browser_evalto find localized variables.
However, for version 5.3.5, the wos_troubleshooting function appears to be completely unprotected.
5. Exploitation Strategy
The goal is to leak the metadata and items of an order that does not belong to the attacker.
Step 1: Authentication
Authenticate as a Subscriber user using the standard WordPress login flow or via wp-cli.
Step 2: Information Leakage Request
Send a POST request to the AJAX endpoint targeting an existing order_id.
Request Details:
- URL:
http://[TARGET]/wp-admin/admin-ajax.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencodedCookie: [Subscriber Cookies]
- Body:
action=wos_troubleshooting&order_id=[TARGET_ORDER_ID]
Step 3: Parsing the Response
The response will be a JSON object. The html key will contain a large string of HTML.
- Look for "Order Meta": This section contains serialized order metadata, often including billing names, emails, and phone numbers.
- Look for "Expected Output": Shows the results of the splitting logic.
- Look for "Item Meta": Details specific product choices/notes.
6. Test Data Setup
- Victim Setup:
- Logged in as Administrator, create a test Customer user (
victim_user). - Create a WooCommerce order with sensitive details (e.g., Billing Address: "123 Secret St", Phone: "555-0199").
- Note the ID of this order (e.g.,
123).
- Logged in as Administrator, create a test Customer user (
- Attacker Setup:
- Create a Subscriber user (
attacker_user). - Log in as the Subscriber.
- Create a Subscriber user (
7. Expected Results
A successful exploit will return a JSON response (HTTP 200) containing:
{
"color": {"r": "...", "g": "...", "b": "..."},
"color_hex": "#...",
"order_id": "123",
"split_method": "...",
"html": "... <h4>Order Meta:</h4><pre>Array\n(\n [_billing_first_name] => Victim\n [_billing_email] => victim@example.com\n [_billing_address_1] => 123 Secret St\n ..."
}
The presence of billing information or item meta belonging to the victim confirms the vulnerability.
8. Verification Steps
After performing the HTTP request, verify the leaked data against the database using wp-cli:
# Verify the leaked billing email matches the order ID targeted
wp post meta list [TARGET_ORDER_ID] --keys=_billing_email --format=json
9. Alternative Approaches
If order_id is not known, an attacker can brute-force IDs starting from 1 (or the current timestamp/sequence) as WooCommerce orders typically use sequential integer IDs in the wp_posts table. Since the response Order Meta section is verbose, even a blind probe is highly effective for data harvesting.
Summary
The Order Splitter for WooCommerce plugin for WordPress is vulnerable to unauthorized access of sensitive order data due to a missing capability check and nonce verification on the 'wos_troubleshooting' AJAX endpoint. This allows authenticated attackers with Subscriber-level access or higher to view other users' order details, including billing names, emails, and shipping addresses, by supplying a target order ID.
Vulnerable Code
// inc/functions-troubleshooting.php lines 3-12 function wos_troubleshooting() { extract($_POST); $ret = array(); $order = wc_get_order($order_id); if (!$order) { wp_send_json_error('Order not found'); } --- // inc/functions-troubleshooting.php line 126 add_action( 'wp_ajax_wos_troubleshooting', 'wos_troubleshooting' );
Security Fix
@@ -1,126 +1,140 @@ <?php function wos_troubleshooting() { - extract($_POST); - $ret = array(); - - $order = wc_get_order($order_id); - if (!$order) { - wp_send_json_error('Order not found'); - } - - global $wc_os_settings, $wos_actions_arr; - - $hex = '#' . wc_os_random_color(); - list($r, $g, $b) = sscanf($hex, "#%02x%02x%02x"); - $ret['color'] = ['r' => $r, 'g' => $g, 'b' => $b]; - $ret['color_hex'] = $hex; - $ret['order_id'] = $order_id; - $ret['split_method'] = $wc_os_settings['wc_os_ie']; - - $meta = wc_os_get_order_meta($order_id); - $wc_os_meta_keys = get_option('wc_os_meta_keys', array()); - $items = $order->get_items(); - $shipping_items = $order->get_items('shipping'); - $split_method_key = $ret['split_method']; - $split_method_label = $wos_actions_arr[ $split_method_key ]['action'] ?? ucfirst(str_replace('_', ' ', $split_method_key)); + $ret = array('html'=>''); - - - $debug_output = []; - - $debug_output[] = '<div class="wos-troubleshoot-method"><h3>Split Method: ' . esc_html($split_method_key) . ' — ' . esc_html($split_method_label) . '</h3></div>'; - $debug_output[] = '<div class="wos-meta-keys"><h4>Configured Meta Keys for Grouping:</h4><pre>' . print_r($wc_os_meta_keys, true) . '</pre></div>'; - - $wc_os_order_splitter = new wc_os_order_splitter; - - $debug_output[] = '<div class="wos-split-order-logic"><h4>Expected Output:</h4><pre>'.print_r($wc_os_order_splitter->split_order_logic($order_id, false, true), true).'</pre></div>'; - - // Generic items listing - foreach ($items as $item_id => $item) { - $product_id = $item->get_product_id(); - $product = wc_get_product($product_id); - $vendor_id = get_post_field('post_author', $product_id); - $vendor_name = get_the_author_meta('display_name', $vendor_id); - - $debug_output[] = '<div style="border:1px solid #ccc; padding:10px; margin-bottom:10px;">'; - - $debug_output[] = '<div style="margin-bottom:5px;"><strong>Item ID:</strong> ' . $item_id . '</div>'; + if ( ! isset($_POST['wc_os_nonce']) || ! wp_verify_nonce($_POST['wc_os_nonce'], 'wc_os_nonce_action') ) { - $debug_output[] = '<div style="margin-bottom:5px;"><strong>Product ID:</strong> ' . $product_id . ' - ' . esc_html($product->get_name()) . '</div>'; + wp_send_json_error(__('Sorry, your nonce did not verify.', 'woo-order-splitter')); - $debug_output[] = '<div style="margin-bottom:5px;"><strong>Vendor ID:</strong> ' . $vendor_id . ' (' . esc_html($vendor_name) . ')</div>'; + } elseif ( ! current_user_can('manage_woocommerce') && ! current_user_can('edit_shop_orders') ) { - $debug_output[] = '<div style="margin-bottom:5px;"><strong>Item Meta:</strong><pre style="margin:0;">' . print_r($item->get_meta_data(), true) . '</pre></div>'; + wp_send_json_error(__('You do not have permission to access this resource.', 'woo-order-splitter')); + } else { + + extract($_POST); + $order_id = isset($_POST['order_id']) ? intval($_POST['order_id']) : 0; + + + $order = wc_get_order($order_id); + if (!$order) { + wp_send_json_error('Order not found'); + } + + global $wc_os_settings, $wos_actions_arr; + + $hex = '#' . wc_os_random_color(); + list($r, $g, $b) = sscanf($hex, "#%02x%02x%02x"); + $ret['color'] = ['r' => $r, 'g' => $g, 'b' => $b]; + $ret['color_hex'] = $hex; + $ret['order_id'] = $order_id; + $ret['split_method'] = $wc_os_settings['wc_os_ie']; + + $meta = wc_os_get_order_meta($order_id); + $wc_os_meta_keys = get_option('wc_os_meta_keys', array()); + $items = $order->get_items(); + $shipping_items = $order->get_items('shipping'); - $debug_output[] = '</div>'; + $split_method_key = $ret['split_method']; + $split_method_label = $wos_actions_arr[ $split_method_key ]['action'] ?? ucfirst(str_replace('_', ' ', $split_method_key)); - - - // Add conditional rendering for known split methods - switch ($ret['split_method']) { - case 'group_by_order_item_meta': - case 'group_by_gf_meta': - $meta_keys = get_option('wc_os_gf_meta_keys', []); - $debug_output[] = '<span style="color:red">[!] Missing Group-by Meta Keys?</span>'; - $debug_output[] = '<pre>' . print_r($meta_keys, true) . '</pre>'; - break; - case 'group_by_attributes_value': - $debug_output[] = '<span style="color:red">[!] Attributes Value Grouping Check Needed</span>'; - break; - case 'group_by_woo_vendors': - $vendor_term = wp_get_post_terms($product_id, 'wcpv_product_vendors'); - $group_letter = ''; - if (!empty($vendor_term)) { - $term_id = $vendor_term[0]->term_id; - foreach ((array) $wc_os_settings['wc_os_woo_vendors'] as $letter => $vendor_ids) { - if (in_array($term_id, (array) $vendor_ids)) { - $group_letter = strtoupper($letter); - break; + $debug_output = []; + + $debug_output[] = '<div class="wos-troubleshoot-method"><h3>Split Method: ' . esc_html($split_method_key) . ' — ' . esc_html($split_method_label) . '</h3></div>'; + $debug_output[] = '<div class="wos-meta-keys"><h4>Configured Meta Keys for Grouping:</h4><pre>' . print_r($wc_os_meta_keys, true) . '</pre></div>'; + + $wc_os_order_splitter = new wc_os_order_splitter; + + $debug_output[] = '<div class="wos-split-order-logic"><h4>Expected Output:</h4><pre>'.print_r($wc_os_order_splitter->split_order_logic($order_id, false, true), true).'</pre></div>'; + + // Generic items listing + foreach ($items as $item_id => $item) { + $product_id = $item->get_product_id(); + $product = wc_get_product($product_id); + $vendor_id = get_post_field('post_author', $product_id); + $vendor_name = get_the_author_meta('display_name', $vendor_id); + + $debug_output[] = '<div style="border:1px solid #ccc; padding:10px; margin-bottom:10px;">'; + + $debug_output[] = '<div style="margin-bottom:5px;"><strong>Item ID:</strong> ' . $item_id . '</div>'; + + $debug_output[] = '<div style="margin-bottom:5px;"><strong>Product ID:</strong> ' . $product_id . ' - ' . esc_html($product->get_name()) . '</div>'; + + $debug_output[] = '<div style="margin-bottom:5px;"><strong>Vendor ID:</strong> ' . $vendor_id . ' (' . esc_html($vendor_name) . ')</div>'; + + $debug_output[] = '<div style="margin-bottom:5px;"><strong>Item Meta:</strong><pre style="margin:0;">' . print_r($item->get_meta_data(), true) . '</pre></div>'; + + + $debug_output[] = '</div>'; + + + + + // Add conditional rendering for known split methods + switch ($ret['split_method']) { + case 'group_by_order_item_meta': + case 'group_by_gf_meta': + $meta_keys = get_option('wc_os_gf_meta_keys', []); + $debug_output[] = '<span style="color:red">[!] Missing Group-by Meta Keys?</span>'; + $debug_output[] = '<pre>' . print_r($meta_keys, true) . '</pre>'; + break; + case 'group_by_attributes_value': + $debug_output[] = '<span style="color:red">[!] Attributes Value Grouping Check Needed</span>'; + break; + case 'group_by_woo_vendors': + $vendor_term = wp_get_post_terms($product_id, 'wcpv_product_vendors'); + $group_letter = ''; + if (!empty($vendor_term)) { + $term_id = $vendor_term[0]->term_id; + foreach ((array) $wc_os_settings['wc_os_woo_vendors'] as $letter => $vendor_ids) { + if (in_array($term_id, (array) $vendor_ids)) { + $group_letter = strtoupper($letter); + break; + } } } - } - $debug_output[] = '<div class="wos-vendor-group"><strong>Vendor Group:</strong> <span class="vendor-group-letter">' . esc_html($group_letter) . '</span></div>'; - $debug_output[] = '<div class="wos-vendor-terms"><strong>Vendor Terms:</strong><pre>' . print_r($vendor_term, true) . '</pre></div>'; - break; - - case 'group_by_vendors': - $vendor_term = wp_get_post_terms($product_id, 'wcpv_product_vendors'); - $debug_output[] = '<strong>Vendor Terms:</strong><pre>' . print_r($vendor_term, true) . '</pre>'; - break; - case 'quantity_split': - $debug_output[] = '<span style="color:red">[!] Quantity Ratio or Threshold Info Missing?</span>'; - break; - case 'group_by_partial_payment': - $debug_output[] = '<span style="color:red">[!] Booking/Partial Payment Meta Missing?</span>'; - break; - case 'subscription_split': - $debug_output[] = '<span style="color:red">[!] Delivery Date Check?</span>'; - break; - case 'group_by_acf_group_fields': - $acf_value = get_post_meta($product_id, 'acf_group_field', true); - $debug_output[] = '<strong>ACF Value:</strong><pre>' . print_r($acf_value, true) . '</pre>'; - break; - default: - $debug_output[] = '<span style="color:red">[!] Custom check not defined for method: ' . esc_html($ret['split_method']) . '</span>'; - break; + $debug_output[] = '<div class="wos-vendor-group"><strong>Vendor Group:</strong> <span class="vendor-group-letter">' . esc_html($group_letter) . '</span></div>'; + $debug_output[] = '<div class="wos-vendor-terms"><strong>Vendor Terms:</strong><pre>' . print_r($vendor_term, true) . '</pre></div>'; + break; + + case 'group_by_vendors': + $vendor_term = wp_get_post_terms($product_id, 'wcpv_product_vendors'); + $debug_output[] = '<strong>Vendor Terms:</strong><pre>' . print_r($vendor_term, true) . '</pre>'; + break; + case 'quantity_split': + $debug_output[] = '<span style="color:red">[!] Quantity Ratio or Threshold Info Missing?</span>'; + break; + case 'group_by_partial_payment': + $debug_output[] = '<span style="color:red">[!] Booking/Partial Payment Meta Missing?</span>'; + break; + case 'subscription_split': + $debug_output[] = '<span style="color:red">[!] Delivery Date Check?</span>'; + break; + case 'group_by_acf_group_fields': + $acf_value = get_post_meta($product_id, 'acf_group_field', true); + $debug_output[] = '<strong>ACF Value:</strong><pre>' . print_r($acf_value, true) . '</pre>'; + break; + default: + $debug_output[] = '<span style="color:red">[!] Custom check not defined for method: ' . esc_html($ret['split_method']) . '</span>'; + break; + } } - } - - $debug_output[] = '<h4>Order Meta:</h4>'; - $debug_output[] = '<pre>' . print_r($meta, true) . '</pre>'; - - $debug_output[] = '<h4>Shipping Items:</h4>'; - $debug_output[] = '<pre>' . print_r($shipping_items, true) . '</pre>'; - $debug_output[] = '<h4>$wc_os_settings:</h4>'; - $debug_output[] = '<pre>' . print_r($wc_os_settings, true) . '</pre>'; - - $ret['html'] = implode("\n", $debug_output); - + $debug_output[] = '<h4>Order Meta:</h4>'; + $debug_output[] = '<pre>' . print_r($meta, true) . '</pre>'; + + $debug_output[] = '<h4>Shipping Items:</h4>'; + $debug_output[] = '<pre>' . print_r($shipping_items, true) . '</pre>'; + + $debug_output[] = '<h4>$wc_os_settings:</h4>'; + $debug_output[] = '<pre>' . print_r($wc_os_settings, true) . '</pre>'; + + $ret['html'] = implode("\n", $debug_output); + } wp_send_json($ret); }
Exploit Outline
To exploit this vulnerability, an attacker first authenticates to the WordPress site as a Subscriber or higher. They then send a POST request to the `/wp-admin/admin-ajax.php` endpoint with the `action` parameter set to `wos_troubleshooting` and the `order_id` parameter set to the ID of the target order. Since the plugin fails to verify user capabilities or validate that the requester owns the order, it returns a JSON object where the `html` key contains a comprehensive dump of the order's metadata (including PII like billing names and emails), product information, and vendor details.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.