Download Monitor <= 5.1.7 - Insecure Direct Object Reference to Unauthenticated Arbitrary Order Completion via 'token' and 'order_id'
Description
The Download Monitor plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 5.1.7 via the executePayment() function due to missing validation on a user controlled key. This makes it possible for unauthenticated attackers to complete arbitrary pending orders by exploiting a mismatch between the PayPal transaction token and the local order, allowing theft of paid digital goods by paying a minimal amount for a low-cost item and using that payment token to finalize a high-value order.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:NTechnical Details
<=5.1.7What Changed in the Fix
Changes introduced in v5.1.8
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-3124 ## 1. Vulnerability Summary The **Download Monitor** plugin (up to 5.1.7) contains an Insecure Direct Object Reference (IDOR) vulnerability in its PayPal payment execution logic. The function `ExecutePaymentListener::executePayment()` uses a user-supplied…
Show full research plan
Exploitation Research Plan: CVE-2026-3124
1. Vulnerability Summary
The Download Monitor plugin (up to 5.1.7) contains an Insecure Direct Object Reference (IDOR) vulnerability in its PayPal payment execution logic. The function ExecutePaymentListener::executePayment() uses a user-supplied PayPal token to verify a payment with PayPal's API. However, it fails to verify that the PayPal transaction associated with that token actually corresponds to the local order_id provided in the request.
Because the code proceeds to call $order->set_completed() regardless of whether the PayPal transaction ID matches the local transaction records (due to a logic error in the transaction loop), an attacker can pay for a low-cost item to obtain a valid "COMPLETED" PayPal token and then use that token to finalize an order for a high-value item without paying the full price.
2. Attack Vector Analysis
- Endpoint: The site's root index (or any page) where the plugin's
ExecutePaymentListeneris active. - Hook: Likely
initortemplate_redirect(viaExecutePaymentListener::run). - HTTP Method:
GET - Vulnerable Parameters:
paypal_action: Must be set toexecute_payment.order_id: The ID of the target (expensive) order to be completed.order_hash: The unique hash of the target order (required to bypass initial checks).token: A valid PayPal Order ID (token) representing a completed payment from a different (cheap) transaction.
- Authentication: None (Unauthenticated).
3. Code Flow
- Entry Point:
src/Shop/Checkout/PaymentGateway/PayPal/ExecutePaymentListener.php:run()- Checks if
$_GET['paypal_action'] === 'execute_payment'.
- Checks if
- Order Retrieval:
executePayment()- Retrieves
$order_idand$order_hashfrom$_GET. - Loads the order:
$order = $order_repo->retrieve_single( $order_id ).
- Retrieves
- PayPal Verification:
- Retrieves
$tokenfrom$_GET['token']. - Instantiates
CaptureOrder. - Sets the PayPal target ID to the user-supplied token:
$capture->set_order_id( $token )(Line 91). - Calls
$capture->captureOrder(), which contacts PayPal to verify the status of that specific token.
- Retrieves
- Logic Failure:
- If PayPal returns a "COMPLETED" status for that
$token, the code enters a loop to update local transactions. - It checks if
$transaction->get_processor_transaction_id() == $response->getId(). - Vulnerability: Even if the transaction ID doesn't match (which it won't for a swapped token), the code proceeds to call
$order->set_completed()(Line 124) outside the transaction matching loop.
- If PayPal returns a "COMPLETED" status for that
- Sink:
$order->set_completed()marks the expensive order as paid and persists it to the database, granting access to the digital goods.
4. Nonce Acquisition Strategy
Based on the source code in src/Shop/Checkout/PaymentGateway/PayPal/ExecutePaymentListener.php, no WordPress nonce is required for this endpoint. The code only checks for the presence of paypal_action, order_id, and order_hash.
5. Exploitation Strategy
The goal is to complete a "Pending" high-value order using a token from a "Completed" low-value order.
Step-by-Step Plan:
- Identify Downloads: Find an expensive download and a cheap/minimal-cost download.
- Create High-Value Order:
- Add the expensive item to the cart.
- Proceed to checkout as a guest.
- Place the order but do not pay.
- Intercept the redirect to PayPal to capture the
order_idandorder_hashfrom the URL parameters.
- Create and Pay for Low-Value Order:
- Add the cheap item to the cart.
- Proceed to checkout and complete the payment via PayPal.
- After successful payment, capture the
tokenparameter from the redirect URL (this is the PayPal Order ID).
- Execute Bypass:
- Construct a
GETrequest to the target site root using theorder_idandorder_hashof the expensive order, but thetokenof the paid cheap order.
- Construct a
Payload Construction:
- URL:
http://localhost:8080/ - Query Parameters:
paypal_action=execute_paymentorder_id=[EXPENSIVE_ORDER_ID]order_hash=[EXPENSIVE_ORDER_HASH]token=[CHEAP_PAYPAL_TOKEN]
6. Test Data Setup
- Plugin Config:
- Enable "Shop" in Download Monitor settings.
- Configure PayPal Gateway (Sandbox mode is sufficient).
- Content Creation:
- Use WP-CLI to create two downloads with different prices:
wp dlm create_download --title="Expensive Product" --price=999(Note: command args are illustrative; standard post creation may be needed).wp dlm create_download --title="Cheap Product" --price=1.
- Use WP-CLI to create two downloads with different prices:
- Order Generation:
- Manually or via script, generate a "Pending" order for the Expensive Product to obtain its ID and Hash.
7. Expected Results
- The server will respond with a
302 Redirectto the "Success" URL associated with the expensive order. - The
order_idprovided in the request will transition frompendingtocompletedin the database. - The user (attacker) will be granted access to the digital files associated with the expensive order.
8. Verification Steps
After sending the exploit request, use WP-CLI to check the order status:
# Verify the expensive order is now 'completed'
wp post get [EXPENSIVE_ORDER_ID] --field=post_status
# Or if stored in custom tables/meta:
wp post meta get [EXPENSIVE_ORDER_ID] _dlm_order_status
9. Alternative Approaches
If order_hash validation is stricter in retrieve_single than it appears:
- Hash Prediction: Check if the order hash is generated using a predictable algorithm (e.g.,
md5(order_id + secret_key)). - Log/Debug Leakage: Check if the plugin logs
order_hashvalues indebug.logor a custom PayPal log file which might be publicly accessible. - Database Leakage: If another vulnerability exists (like a minor SQLi), use it to extract the
order_hash.
However, since the attacker is the one creating the expensive order (guest checkout), they should naturally have the hash via the standard checkout flow.
Summary
The Download Monitor plugin for WordPress is vulnerable to an Insecure Direct Object Reference (IDOR) in its PayPal payment processing logic. An unauthenticated attacker can complete high-value orders by providing a valid PayPal transaction token from a low-cost purchase, as the plugin fails to verify that the PayPal token corresponds to the specific local order being finalized.
Vulnerable Code
// src/Shop/Checkout/PaymentGateway/PayPal/ExecutePaymentListener.php private function executePayment() { /** * Get order */ $order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0; $order_hash = isset( $_GET['order_hash'] ) ? sanitize_text_field( wp_unslash($_GET['order_hash']) ) : ''; if ( empty( $order_id ) || empty( $order_hash ) ) { $this->execute_failed( $order_id, $order_hash ); } /** @var \WPChill\DownloadMonitor\Shop\Order\Repository $order_repo */ $order_repo = Services::get()->service( 'order_repository' ); try { $order = $order_repo->retrieve_single( $order_id ); } catch ( \Exception $exception ) { $this->execute_failed( $order_id, $order_hash ); return; } /** * Get payment identifier */ $token = ''; if ( isset( $_GET['token'] ) ) { $token = sanitize_text_field( wp_unslash( $_GET['token'] ) ); } /** * Execute the payement */ try { $capture = new CaptureOrder(); $capture->set_client( $this->gateway->get_api_context() ) ->set_order_id( $token ); $response = $capture->captureOrder(); // if payment is not approved, exit; if ( $response->getStatus() !== "COMPLETED" ) { throw new Exception( sprintf( "Execute payment state is %s", $response->getStatus() ) ); } /** * Update transaction in local database */ // update the order status to 'completed' $transactions = $order->get_transactions(); foreach ( $transactions as $transaction ) { if ( $transaction->get_processor_transaction_id() == $response->getId() ) { $transaction->set_status( Services::get()->service( 'order_transaction_factory' )->make_status( 'success' ) ); $transaction->set_processor_status( $response->getStatus() ); // ... (truncated) $order->set_transactions( $transactions ); break; } } // set order as completed, this also persists the order $order->set_completed();
Security Fix
@@ -38,6 +38,7 @@ if ( empty( $order_id ) || empty( $order_hash ) ) { $this->execute_failed( $order_id, $order_hash ); + return; } /** @var \WPChill\DownloadMonitor\Shop\Order\Repository $order_repo */ @@ -53,16 +54,41 @@ return; } + // Verify order_hash against the retrieved order (timing-safe) to prevent IDOR. + if ( ! hash_equals( (string) $order->get_hash(), (string) $order_hash ) ) { + $this->execute_failed( $order_id, $order_hash ); + return; + } + /** - * Get payment identifier + * Get payment identifier (PayPal order ID / token) */ $token = ''; if ( isset( $_GET['token'] ) ) { $token = sanitize_text_field( wp_unslash( $_GET['token'] ) ); } + if ( empty( $token ) ) { + $this->execute_failed( $order_id, $order_hash ); + return; + } + + // Bind token to this order: token must match one of this order's transactions (PayPal order ID). + $transactions = $order->get_transactions(); + $token_belongs_to_order = false; + foreach ( $transactions as $transaction ) { + if ( hash_equals( (string) $transaction->get_processor_transaction_id(), (string) $token ) ) { + $token_belongs_to_order = true; + break; + } + } + if ( ! $token_belongs_to_order ) { + $this->execute_failed( $order_id, $order_hash ); + return; + } + /** - * Execute the payement + * Execute the payment */ try { @@ -70,7 +96,15 @@ $capture->set_client( $this->gateway->get_api_context() ) ->set_order_id( $token ); - $response = $capture->captureOrder(); + $capture_result = $capture->captureOrder(); + + // Handle capture failures safely (e.g. network error, invalid token). + if ( null === $capture_result || ! $capture_result->has_response() ) { + $this->execute_failed( $order->get_id(), $order->get_hash() ); + return; + } + + $response = $capture_result; // if payment is not approved, exit; if ( $response->getStatus() !== "COMPLETED" ) { @@ -80,11 +114,11 @@ /** * Update transaction in local database */ - - // update the order status to 'completed' - $transactions = $order->get_transactions(); + // Update the transaction that belongs to this token (already validated above). + $transaction_updated = false; + $transactions = $order->get_transactions(); foreach ( $transactions as $transaction ) { - if ( $transaction->get_processor_transaction_id() == $response->getId() ) { + if ( hash_equals( (string) $transaction->get_processor_transaction_id(), (string) $token ) ) { $transaction->set_status( Services::get()->service( 'order_transaction_factory' )->make_status( 'success' ) ); $transaction->set_processor_status( $response->getStatus() ); @@ -95,9 +129,15 @@ } $order->set_transactions( $transactions ); + $transaction_updated = true; break; } + } + // Only complete the order if we actually updated a matching transaction (prevents token/amount mismatch). + if ( ! $transaction_updated ) { + $this->execute_failed( $order->get_id(), $order->get_hash() ); + return; }
Exploit Outline
The exploit leverages a logic error where the plugin checks if a PayPal token is "COMPLETED" but doesn't verify if it belongs to the target order. 1. Identify a high-value digital product. Add it to the cart, proceed to checkout as a guest, and initiate payment to generate a 'Pending' order. 2. Record the `order_id` and `order_hash` for this expensive order from the URL or intercept the initial redirect to PayPal. 3. Identify a low-value digital product ($1 or less). Complete the purchase and payment via PayPal. 4. Capture the `token` parameter provided by PayPal during the return redirect for the cheap purchase. 5. Execute a GET request to the site's root with the parameters: `paypal_action=execute_payment`, `order_id=[Expensive Order ID]`, `order_hash=[Expensive Order Hash]`, and `token=[Paid Cheap Token]`. 6. The plugin will verify the cheap token with PayPal, confirm it is 'COMPLETED', and mistakenly mark the expensive order as paid, allowing the attacker to download the expensive product.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.