CVE-2025-13930

Checkout Field Manager (Checkout Manager) for WooCommerce <= 7.8.5 - Missing Authorization to Unauthenticated Arbitrary Attachment Deletion

mediumMissing Authorization
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
7.8.6
Patched in
1d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=7.8.5
PublishedFebruary 18, 2026
Last updatedFebruary 19, 2026

What Changed in the Fix

Changes introduced in v7.8.6

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

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 `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 in lib/class-upload.php)
  • Payload Parameters:
    • nonce: A valid wooccm_upload nonce.
    • 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 missing nopriv hook 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

  1. Entry: The request hits admin-ajax.php with action=wooccm_order_attachment_update.
  2. Nonce Check: check_admin_referer( 'wooccm_upload', 'nonce' ) verifies the wooccm_upload nonce.
  3. Flawed Logic:
    • $array1 is populated from all_attachments_ids.
    • $array2 is populated from delete_attachments_ids.
    • $attachment_ids = array_diff( $array1, $array2 ); calculates which IDs are in all but NOT in delete.
    • Crucial: To delete an attachment, the attacker includes its ID in all_attachments_ids and omits it from delete_attachments_ids.
  4. Bypassed Ownership Check:
    • The code retrieves the order via $order = wc_get_order( $post_parent );.
    • $current_user = wp_get_current_user(); (ID is 0 for guests).
    • $order_user_id = $order->get_user_id(); (ID is 0 for guest orders).
    • $is_current_user_order_equal_user_id = $current_user->ID === $order_user_id; (evaluates to 0 === 0, which is true).
    • if ( ! $user_has_capabilities && ! $is_current_user_order_equal_user_id ) evaluates to if ( !false && !true ) -> false.
  5. 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.

  1. Identify Trigger: The plugin typically enqueues its scripts on the WooCommerce Checkout page or the Order Received page.
  2. Navigation: Use the browser to navigate to the checkout page.
  3. Extraction:
    • Navigate to: http://localhost:8080/checkout/
    • Execute JS: browser_eval("window.wooccm_upload?.nonce")
  4. 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.

  1. Step 1: Setup Target
    • Create a guest order and upload an attachment to it.
    • Note the Attachment ID.
  2. Step 2: Obtain Nonce
    • Navigate to the checkout page and extract window.wooccm_upload.nonce.
  3. Step 3: Trigger Deletion
    • Send a POST request to admin-ajax.php.
    • Logic: To delete ID 123, it must be in all_attachments_ids and NOT in delete_attachments_ids.

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

  1. WooCommerce Setup: Ensure a product exists and WooCommerce checkout is functional.
  2. Create Order: As a guest, add a product to the cart and complete the checkout.
  3. Upload File: Use the wooccm_checkout_attachment_upload action (which is nopriv) 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"
    
  4. 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

  1. Database Check: Use WP-CLI to check if the attachment still exists:
    wp post exists [ATTACHMENT_ID]
    
    If the exploit worked, this should return an empty response or an error code.
  2. File Check: Verify the file is gone from the uploads/wooccm_uploads directory.

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 logic if ( ! $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.
Research Findings
Static analysis — not yet PoC-verified

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

--- a/lib/class-upload.php
+++ b/lib/class-upload.php
@@ -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.