Checkout Field Manager (Checkout Manager) for WooCommerce <= 7.8.5 - Missing Authorization to Unauthenticated Arbitrary Attachment Deletion
Description
The Checkout Field Manager (Checkout Manager) for WooCommerce plugin for WordPress is vulnerable to authorization bypass in versions up to, and including, 7.8.5. This is due to the plugin not properly verifying that a user is authorized to delete an attachment combined with flawed guest order ownership validation. This makes it possible for unauthenticated attackers to delete attachments associated with guest orders using only the publicly available wooccm_upload nonce and attachment ID.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=7.8.5What Changed in the Fix
Changes introduced in v7.8.6
Source Code
WordPress.org SVNThis research plan targets CVE-2025-13930, a missing authorization vulnerability in the "Checkout Field Manager (Checkout Manager) for WooCommerce" plugin. The vulnerability allows an attacker to delete arbitrary attachments associated with guest orders by exploiting flawed ownership logic in the `a…
Show full research plan
This research plan targets CVE-2025-13930, a missing authorization vulnerability in the "Checkout Field Manager (Checkout Manager) for WooCommerce" plugin. The vulnerability allows an attacker to delete arbitrary attachments associated with guest orders by exploiting flawed ownership logic in the ajax_delete_attachment function.
1. Vulnerability Summary
The QuadLayers\WOOCCM\Upload::ajax_delete_attachment function, registered via the wp_ajax_wooccm_order_attachment_update action, is intended to allow users to manage files uploaded during the checkout process. However, it fails to verify if the user has permission to delete a specific attachment.
The plugin checks if the current user's ID matches the order's user ID. For guest orders, the order's user ID is 0. Since unauthenticated users also have a WordPress user ID of 0, the condition 0 === 0 evaluates to true, bypassing ownership validation and allowing any guest to delete attachments belonging to any guest order.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
wooccm_order_attachment_update(registered inlib/class-upload.php) - Payload Parameters:
nonce: A validwooccm_uploadnonce.all_attachments_ids: A comma-separated list of attachment IDs to "keep" (the logic is flawed; see Code Flow).delete_attachments_ids: A comma-separated list of attachment IDs.
- Authentication: Unauthenticated (or Guest). Although only the
wp_ajax_hook is visible in the provided snippet, the vulnerability description confirms unauthenticated access, implying either a missingnoprivhook in the snippet or the availability of this action to all users via front-end order management. - Preconditions: An attachment must exist and be associated with a guest order (
shop_order).
3. Code Flow
- Entry: The request hits
admin-ajax.phpwithaction=wooccm_order_attachment_update. - Nonce Check:
check_admin_referer( 'wooccm_upload', 'nonce' )verifies thewooccm_uploadnonce. - Flawed Logic:
$array1is populated fromall_attachments_ids.$array2is populated fromdelete_attachments_ids.$attachment_ids = array_diff( $array1, $array2 );calculates which IDs are inallbut NOT indelete.- Crucial: To delete an attachment, the attacker includes its ID in
all_attachments_idsand omits it fromdelete_attachments_ids.
- Bypassed Ownership Check:
- The code retrieves the order via
$order = wc_get_order( $post_parent );. $current_user = wp_get_current_user();(ID is0for guests).$order_user_id = $order->get_user_id();(ID is0for guest orders).$is_current_user_order_equal_user_id = $current_user->ID === $order_user_id;(evaluates to0 === 0, which istrue).if ( ! $user_has_capabilities && ! $is_current_user_order_equal_user_id )evaluates toif ( !false && !true )->false.
- The code retrieves the order via
- Sink:
wp_delete_attachment( $attachtoremove )is called on the provided ID.
4. Nonce Acquisition Strategy
The nonce is localized for the frontend using wp_localize_script. Based on build/frontend/js/index.js, the variable name is wooccm_upload.
- Identify Trigger: The plugin typically enqueues its scripts on the WooCommerce Checkout page or the Order Received page.
- Navigation: Use the browser to navigate to the checkout page.
- Extraction:
- Navigate to:
http://localhost:8080/checkout/ - Execute JS:
browser_eval("window.wooccm_upload?.nonce")
- Navigate to:
- Action String: The nonce is created using the action string
'wooccm_upload'.
5. Exploitation Strategy
The goal is to delete a specific attachment ID (e.g., 123) associated with a guest order.
- Step 1: Setup Target
- Create a guest order and upload an attachment to it.
- Note the Attachment ID.
- Step 2: Obtain Nonce
- Navigate to the checkout page and extract
window.wooccm_upload.nonce.
- Navigate to the checkout page and extract
- Step 3: Trigger Deletion
- Send a POST request to
admin-ajax.php. - Logic: To delete ID
123, it must be inall_attachments_idsand NOT indelete_attachments_ids.
- Send a POST request to
Request Details:
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Method: POST
- Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=wooccm_order_attachment_update&nonce=[NONCE]&all_attachments_ids=123&delete_attachments_ids=
6. Test Data Setup
- WooCommerce Setup: Ensure a product exists and WooCommerce checkout is functional.
- Create Order: As a guest, add a product to the cart and complete the checkout.
- Upload File: Use the
wooccm_checkout_attachment_uploadaction (which isnopriv) to upload a file to the order, or manually associate an attachment with a guest order via WP-CLI for testing:# Create attachment associated with a guest order (ID 10) wp post create --post_type=attachment --post_parent=10 --post_title="Sensitive File" - Identify ID: Get the ID of the created attachment.
7. Expected Results
- Response: The server should return a JSON success message:
{"success":true,"data":"Deleted successfully."}. - Impact: The attachment with the specified ID is permanently deleted from the WordPress media library and the filesystem.
8. Verification Steps
- Database Check: Use WP-CLI to check if the attachment still exists:
If the exploit worked, this should return an empty response or an error code.wp post exists [ATTACHMENT_ID] - File Check: Verify the file is gone from the
uploads/wooccm_uploadsdirectory.
9. Alternative Approaches
If the wp_ajax_wooccm_order_attachment_update action is strictly limited to logged-in users (meaning nopriv is truly absent), test as a Subscriber.
- Logged-in users still bypass the ownership check if they target a guest order, because the Subscriber's ID (e.g.,
5) will not match the guest order's ID (0), BUT the logicif ( ! $is_user_logged && ! $is_session_email_equal_order_email )(Line 144) will trigger for logged-in users. - The attacker would need to match the session email to the order's billing email or exploit the fact that guests bypass the entire first block of checks. Focus on the Unauthenticated (Guest) path first, as it is the most severe and matches the CVE description.
Summary
The Checkout Field Manager plugin for WooCommerce is vulnerable to unauthenticated arbitrary attachment deletion due to flawed ownership logic in the 'ajax_delete_attachment' function. Attackers can exploit a logic error where the comparison between an unauthenticated user (ID 0) and a guest order (ID 0) evaluates to true, bypassing authorization checks to delete files associated with orders.
Vulnerable Code
// lib/class-upload.php:127 public function ajax_delete_attachment() { if ( ! empty( $_REQUEST ) && check_admin_referer( 'wooccm_upload', 'nonce' ) ) { $array1 = explode( ',', sanitize_text_field( isset( $_REQUEST['all_attachments_ids'] ) ? wp_unslash( $_REQUEST['all_attachments_ids'] ) : '' ) ); $array2 = explode( ',', sanitize_text_field( isset( $_REQUEST['delete_attachments_ids'] ) ? wp_unslash( $_REQUEST['delete_attachments_ids'] ) : '' ) ); if ( empty( $array1 ) || empty( $array2 ) ) { wp_send_json_error( esc_html__( 'No attachment selected.', 'woocommerce-checkout-manager' ) ); } $attachment_ids = array_diff( $array1, $array2 ); if ( ! empty( $attachment_ids ) ) { foreach ( $attachment_ids as $key => $attachtoremove ) { // ... (truncated validation checks) ... $order = wc_get_order( $post_parent ); $current_user = wp_get_current_user(); $session_handler = WC()->session; $is_user_logged = 0 === $current_user->ID; $order_email = $order->get_billing_email(); $session_customer_email = $session_handler->get( 'customer' )['email']; $is_session_email_equal_order_email = $order_email === $session_customer_email; if ( ! $is_user_logged && ! $is_session_email_equal_order_email ) { wp_send_json_error( esc_html__( 'You must be logged in.', 'woocommerce-checkout-manager' ) ); } $order_user_id = $order->get_user_id(); $user_has_capabilities = current_user_can( 'administrator' ) || current_user_can( 'edit_others_shop_orders' ) || current_user_can( 'delete_others_shop_orders' ); $is_current_user_order_equal_user_id = $current_user->ID === $order_user_id; if ( ! $user_has_capabilities && ! $is_current_user_order_equal_user_id ) { wp_send_json_error( esc_html__( 'This is not your order.', 'woocommerce-checkout-manager' ) ); } wp_delete_attachment( $attachtoremove ); } } wp_send_json_success( 'Deleted successfully.', 'woocommerce-checkout-manager' ); } }
Security Fix
@@ -141,13 +141,13 @@ $session_handler = WC()->session; - $is_user_logged = 0 === $current_user->ID; + $is_user_guest = 0 === $current_user->ID; $order_email = $order->get_billing_email(); $session_customer_email = $session_handler->get( 'customer' )['email']; $is_session_email_equal_order_email = $order_email === $session_customer_email; - if ( ! $is_user_logged && ! $is_session_email_equal_order_email ) { + if ( $is_user_guest && ! $is_session_email_equal_order_email ) { wp_send_json_error( esc_html__( 'You must be logged in.', 'woocommerce-checkout-manager' ) ); } $order_user_id = $order->get_user_id(); $user_has_capabilities = current_user_can( 'administrator' ) || current_user_can( 'edit_others_shop_orders' ) || current_user_can( 'delete_others_shop_orders' ); - $is_current_user_order_equal_user_id = $current_user->ID === $order_user_id; + $is_current_user_order_equal_user_id = ! empty( $current_user->ID ) && $current_user->ID === $order_user_id; if ( ! $user_has_capabilities && ! $is_current_user_order_equal_user_id ) { wp_send_json_error( esc_html__( 'This is not your order.', 'woocommerce-checkout-manager' ) ); }
Exploit Outline
1. Obtain a valid 'wooccm_upload' nonce by visiting the WooCommerce checkout page where the plugin scripts are enqueued. 2. Identify the target attachment ID associated with a guest order (an order where user_id is 0). 3. Craft an unauthenticated AJAX request to the 'wooccm_order_attachment_update' action. 4. Populate the 'all_attachments_ids' parameter with the target attachment ID and leave the 'delete_attachments_ids' parameter empty or ensure it does not contain the target ID. 5. Send the POST request to admin-ajax.php. The plugin's logic will calculate the difference (all minus delete), find the target ID, and proceed to the ownership check. 6. Because both the current user and the guest order owner have an ID of 0, the plugin mistakenly validates ownership and executes wp_delete_attachment(target_id).
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.