Accept Stripe Payments <= 2.0.98 - Unauthenticated Payment Bypass
Description
The Accept Stripe Payments plugin for WordPress is vulnerable to Payment Bypasses in all versions up to, and including, 2.0.98. This makes it possible for unauthenticated attackers to bypass payments.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=2.0.98What Changed in the Fix
Changes introduced in v2.0.99
Source Code
WordPress.org SVN# Research Plan: CVE-2026-42752 - Accept Stripe Payments Payment Bypass ## 1. Vulnerability Summary The **Accept Stripe Payments** plugin (<= 2.0.98) is vulnerable to an unauthenticated payment bypass. The core issue resides in the handling of payment results in `ASP_Process_IPN_NG::handle_next_act…
Show full research plan
Research Plan: CVE-2026-42752 - Accept Stripe Payments Payment Bypass
1. Vulnerability Summary
The Accept Stripe Payments plugin (<= 2.0.98) is vulnerable to an unauthenticated payment bypass. The core issue resides in the handling of payment results in ASP_Process_IPN_NG::handle_next_action_results. The plugin allows a user-controlled parameter, is_live, to determine whether the transaction should be validated against Stripe's Live or Test API keys.
An attacker can complete a transaction using a test card via the plugin's test configuration and then force the server to validate that "test" payment as a successful purchase by setting is_live=false in the results handler, even for products intended for live production.
2. Attack Vector Analysis
- Endpoints:
wp-admin/admin-ajax.php?action=asp_pp_save_form_data(Session Setup)wp-admin/admin-ajax.php?action=asp_next_action_results(Bypass Trigger)
- Vulnerable Parameter:
is_live(GET) andpayment_intent(GET). - Authentication: None required (
noprivAJAX actions). - Preconditions:
- A product must be created.
- Stripe Test API keys must be configured in the plugin settings (often left in place after development or used for "Sandbox" mode).
3. Code Flow
- Entry Point 1 (Session Setup): The attacker calls
asp_pp_save_form_data. The plugin saves product details (likeproduct_id) into a server-side session transient (asp_pp_form_data). - Entry Point 2 (The Bypass): The attacker calls
asp_next_action_results. - Key Selection (
includes/class-asp-process-ipn-ng.php):$is_live = isset( $_GET['is_live'] ) ? sanitize_text_field( stripslashes ( $_GET['is_live'] ) ) : ''; $is_live = 'false' === $is_live ? 0 : 1; // ... $key = $is_live ? $this->asp_class->APISecKey : $this->asp_class->APISecKeyTest; \Stripe\Stripe::setApiKey( $key ); - Verification: The plugin retrieves the
payment_intentusing the selected$key. If the attacker provides apayment_intentcreated in the Stripe test environment and setsis_live=false, the retrieval succeeds. - Fulfillment: The plugin calls
process_ipn(), which validates the amount and fulfills the order (e.g., provides download links or marks the order as paid).
4. Nonce Acquisition Strategy
The action asp_pp_save_form_data is used to initialize the session. While its implementation is not fully shown, it is likely authenticated by asp_pp_ajax_nonce.
- Identify Shortcode: The plugin uses
[asp_product id="ID"]to display products. - Create Page: Create a test page containing the product shortcode.
- Extract Nonce:
- Navigate to the product page.
- The nonce and other metadata are localized via
wp_localize_script. - In the source of
public/views/templates/default/payment-popup.php, variables are printed into the global scope. - Use
browser_evalto find the nonce:browser_eval("window.stripe_payments_vars?.nonce")or check forasp_pp_ajax_noncein the script tags.
5. Exploitation Strategy
Step 1: Session Setup
Call asp_pp_save_form_data to associate the current session with the target product.
- URL:
http://vulnerable-test.local/wp-admin/admin-ajax.php - Method: POST
- Payload (application/x-www-form-urlencoded):
action:asp_pp_save_form_datanonce:[EXTRACTED_NONCE]asp_product_id:[TARGET_PRODUCT_ID]
Step 2: Bypass Trigger
Invoke the results handler with is_live=false and a test payment_intent.
- URL:
http://vulnerable-test.local/wp-admin/admin-ajax.php?action=asp_next_action_results&is_live=false&payment_intent=[TEST_PI_ID] - Method: GET
- Note: In a real-world scenario,
[TEST_PI_ID]is obtained by completing a checkout using Stripe's test card (e.g.,4242...) against the site's public test key. For a PoC in an isolated environment, if Stripe API connectivity is unavailable, the goal is to demonstrate that the code switches toAPISecKeyTestbased on the GET parameter.
6. Test Data Setup
- Configure Plugin:
# Set dummy Stripe keys wp option update AcceptStripePayments-settings '{"api_key_test_public":"pk_test_dummy","api_key_test_secret":"sk_test_dummy","api_key_live_public":"pk_live_dummy","api_key_live_secret":"sk_live_dummy"}' --format=json - Create Product:
wp post create --post_type=asp-products --post_title="Expensive Digital Asset" --post_status=publish # Capture the generated Product ID - Place Shortcode:
wp post create --post_type=page --post_title="Store" --post_content='[asp_product id="[ID]"]' --post_status=publish
7. Expected Results
- The
asp_next_action_resultscall should triggerprocess_ipn. - The plugin should redirect to the "Thank You" page (
checkout_urlsetting). - The plugin's debug log (if enabled via
ASP_Debug_Logger) should show "Payment processing started" using the test key despite the product potentially being live.
8. Verification Steps
After the HTTP requests, verify the order creation via WP-CLI:
# Check for new orders (post_type: stripe_order)
wp post list --post_type=stripe_order --orderby=post_date --order=DESC
The order status should be marked as complete/paid even though no real funds were transferred.
9. Alternative Approaches
If asp_pp_save_form_data is strictly nonced or session-locked, examine the wp_loaded entry point in includes/class-asp-process-ipn-ng.php:
$process_ipn = filter_input( INPUT_POST, 'asp_process_ipn', FILTER_SANITIZE_NUMBER_INT );
if ( $process_ipn ) {
add_action( 'wp_loaded', array( $this, 'process_ipn' ), 2147483647 );
}
This path directly hits process_ipn() without the AJAX wrapper, potentially allowing an attacker to bypass the is_live check entirely if they can inject POST data that process_ipn trusts. Check if process_ipn verifies the payment_intent independently of the is_live logic used in the AJAX handler.
Summary
The Accept Stripe Payments plugin for WordPress is vulnerable to an unauthenticated payment bypass due to the use of a user-controlled parameter to select the Stripe API environment (Live vs. Test). An attacker can submit a Stripe test-mode payment intent and force the plugin to validate it against test credentials by setting 'is_live=false', resulting in order fulfillment for live products without actual payment.
Vulnerable Code
// includes/class-asp-process-ipn-ng.php line 64 $pi_id = isset( $_GET['payment_intent'] ) ? sanitize_text_field( stripslashes ( $_GET['payment_intent'] ) ) : ''; $is_live = isset( $_GET['is_live'] ) ? sanitize_text_field( stripslashes ( $_GET['is_live'] ) ) : ''; $is_live = 'false' === $is_live ? 0 : 1; --- // includes/class-asp-process-ipn-ng.php line 81 try { ASPMain::load_stripe_lib(); $key = $is_live ? $this->asp_class->APISecKey : $this->asp_class->APISecKeyTest; \Stripe\Stripe::setApiKey( $key );
Security Fix
@@ -3,7 +3,7 @@ /** * Plugin Name: Accept Stripe Payments * Description: Easily accept credit card payments via Stripe payment gateway in WordPress. - * Version: 2.0.98 + * Version: 2.0.99 * Author: Tips and Tricks HQ, wptipsntricks * Author URI: https://www.tipsandtricks-hq.com/ * Plugin URI: https://s-plugins.com @@ -19,7 +19,7 @@ exit; } -define( 'WP_ASP_PLUGIN_VERSION', '2.0.98' ); +define( 'WP_ASP_PLUGIN_VERSION', '2.0.99' ); define( 'WP_ASP_MIN_PHP_VERSION', '7.4' ); define( 'WP_ASP_PLUGIN_URL', plugins_url( '', __FILE__ ) ); define( 'WP_ASP_PLUGIN_PATH', plugin_dir_path( __FILE__ ) ); @@ -264,6 +264,11 @@ $this->billing_details_obj = $bd; + $cd = new stdClass; + $cd->name = $b_name; + $cd->email = $b_email; + $this->customer_obj = $cd; + //Shipping details $same_addr = $ipn_ng_class->get_post_var( 'asp_same-bill-ship-addr' ); @@ -700,6 +700,9 @@ $zero_value_id = str_replace( '.', '', uniqid( 'free_', true ) ); $coupon['zero_value_id'] = $zero_value_id; + // Saving applied coupon signature data so that it can be verified later. + ASP_Utils_Bot_Mitigation::record_full_discount_signature_data($product_id, $coupon_code, $zero_value_id); + wp_send_json( $coupon ); } @@ -215,6 +215,12 @@ //ASP_Debug_Logger::log( 'Original $_POST data: ' . $post_data_str ); } + $nonce = $this->get_post_var( 'asp_payment_form_nonce' ); + if (empty($nonce) || !wp_verify_nonce($nonce, 'asp_payment_form_nonce')){ + // ASP_Debug_Logger::log( 'Nonce verification failed!', false); + wp_die( __('Nonce verification failed!', 'stripe-payments') ); + } + do_action( 'asp_ng_before_payment_processing', $post_data ); $this->sess = ASP_Session::get_instance(); @@ -812,6 +818,14 @@ //this is zero-value transaction $coupon_code = $this->get_post_var( 'asp_coupon-code' ); $coupon_code = sanitize_text_field( stripslashes( $coupon_code )); + $prod_id = $this->item->get_product_id(); + + // Check coupon signature data + if( empty(ASP_Utils_Bot_Mitigation::is_full_discount_signature_data_valid($prod_id, $coupon_code, $pi)) ){ + // Signature is invalid. + wp_die(sprintf(__( "Error! Invalid transaction ID: %s", 'stripe-payments' ), $pi)); + } + if ( empty( $coupon_code ) ) { return $p_data; } @@ -829,8 +843,6 @@ return $p_data; } - $prod_id = $this->item->get_product_id(); - $order = new ASP_Order_Item(); if ( $order->can_create( $prod_id ) ) { @@ -535,6 +535,18 @@ if ( ! $this->coupon ) { return false; } + + // First check if coupon option is enabled for this product. (the settings in edit product page) + $coupons_enabled_for_product = get_post_meta( $this->post_id, 'asp_product_coupons_setting', true ); + if ( $coupons_enabled_for_product === '' || $coupons_enabled_for_product == '2') { + $coupons_enabled_for_product = $this->asp_main->get_setting( 'coupons_enabled' ); + } + if ( empty( $coupons_enabled_for_product ) ){ + $this->last_error = __( 'Applying coupon is not allowed for this product.', 'stripe-payments' ); + $this->coupon = false; + return false; + } + //check if coupon is allowed for the product $only_for_allowed_products = get_post_meta( $this->coupon['id'], 'asp_coupon_only_for_allowed_products', true ); if ( $only_for_allowed_products ) { @@ -140,7 +140,81 @@ return true; } - + + public static function get_full_discount_signature_data() { + $full_discount_signature_arr = get_transient('asp_full_discount_signature'); + if (!isset($full_discount_signature_arr) || empty($full_discount_signature_arr)) { + $full_discount_signature_arr = array(); + } + return $full_discount_signature_arr; + } + + public static function record_full_discount_signature_data($product_id, $coupon_code, $txn_id) { + $ip_address = ASP_Utils::get_user_ip_address(); + //$current_wp_time = current_time('mysql'); + + if (!isset($ip_address) || empty($ip_address)) { + ASP_Debug_Logger::log('IP address value missing (could not read the IP address of the user). Cannot record this full discount signature. The extra security check later may fail.', false); + return; + } + + $full_discount_signature_arr = ASP_Utils_Bot_Mitigation::get_full_discount_signature_data(); + + $hp_key = ASP_Utils_Bot_Mitigation::get_asp_hash_private_key_one(); + + $index = $product_id. '_' .$ip_address. '_' .$txn_id; + $signature = sha1($hp_key.$product_id.$coupon_code.$ip_address.$txn_id); + $full_discount_signature_arr[$index] = $signature; + + // TODO: Debug Purpose Only. + //ASP_Debug_Logger::log('Index: ' . $index . ', Signature: ' . $signature . ', IP Address: ' . $ip_address . ' Txn ID: '. $txn_id); + //ASP_Debug_Logger::log_array_data($full_discount_signature_arr); + + //Save the page load signature data with an expiry of 1 hour. + set_transient('asp_full_discount_signature', $full_discount_signature_arr, 3600); + } + + public static function is_full_discount_signature_data_valid($product_id, $coupon_code, $txn_id) { + $full_discount_signature_arr = get_transient('asp_full_discount_signature'); + if (!isset($full_discount_signature_arr) || empty($full_discount_signature_arr)) { + ASP_Debug_Logger::log( 'Full discount signature check - currently there are no entries in the saved data for full discount signature.', false ); + return false; + } + + $ip_address_to_check = ASP_Utils::get_user_ip_address(); + if (!isset($ip_address_to_check) || empty($ip_address_to_check)) { + ASP_Debug_Logger::log( 'Full discount signature check - IP address value for this request is missing.', false ); + return false; + } + + $index = $product_id. '_'.$ip_address_to_check. '_' .$txn_id; + + if (!isset($full_discount_signature_arr[$index]) || empty ($full_discount_signature_arr[$index])){ + ASP_Debug_Logger::log( 'Full discount check - cannot find this Product ID and IP address index ('.$index.') in the saved data.', false ); + return false; + } + + $hp_key = ASP_Utils_Bot_Mitigation::get_asp_hash_private_key_one(); + + $expected_signature = $full_discount_signature_arr[$index]; + $received = sha1($hp_key.$product_id.$coupon_code.$ip_address_to_check.$txn_id); + + // TODO: Debug Purpose Only. + //ASP_Debug_Logger::log('Index: ' . $index . ', Signature Received: ' . $received . ', Expected: ' . $expected_signature, true); + //ASP_Debug_Logger::log_array_data($full_discount_signature_arr); + + if (!hash_equals($expected_signature, $received)){ + //Mis-match + ASP_Debug_Logger::log( 'Full discount check - the signature hash does not match.', false ); + return false; + } + + //Entry for the received signature exists + ASP_Debug_Logger::log( 'Full discount check done!' ); + + return true; + } + public static function get_request_limit_count_data() { $asp_request_usage_count = get_transient('asp_request_usage_count'); if (!isset($asp_request_usage_count) || empty($asp_request_usage_count)) { @@ -513,6 +513,7 @@ <input type="hidden" id="btn-uniq-id" name="btn_uniq_id" value="<?php echo ! empty( $a['btn_uniq_id'] ) ? esc_attr( $a['btn_uniq_id'] ) : ''; ?>"> <input type="hidden" id="product-id" name="product_id" value="<?php echo esc_attr( $a['prod_id'] ); ?>"> <input type="hidden" name="process_ipn" value="1"> + <input type="hidden" value="<?php echo esc_attr(wp_create_nonce('asp_payment_form_nonce')) ?>" name="payment_form_nonce"> <input type="hidden" name="is_live" value="<?php echo $a['is_live'] ? '1' : '0'; ?>"> <?php if ( $a['data']['url'] ) { ?> <input type="hidden" name="item_url" value="<?php echo esc_attr( $a['data']['url'] ); ?>"> @@ -5,7 +5,7 @@ Requires at least: 6.0 Tested up to: 7.0 Requires PHP: 7.4 -Stable tag: 2.0.98 +Stable tag: 2.0.99 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -157,6 +157,12 @@ == Changelog == += 2.0.99 = +- Added a server-side check to verify that the coupon feature is enabled before attempting to apply a coupon code. +- Added an additional verification check for 100% discount coupons. +- Added nonce verification to the form submission process. +- Fixed a minor PHP warning on the checkout result page during 100% discount checkout scenarios. + = 2.0.98 = - Added prefixes to the Gutenberg block variables to prevent potential conflicts with other plugins. - Added new filter hook 'asp_process_ipn_order_status' to allow modification of the order status before changing the order status in the IPN processing code.
Exploit Outline
The exploit involves two main steps: session initialization and payment environment manipulation. First, an unauthenticated attacker calls the 'asp_pp_save_form_data' AJAX endpoint to associate their session with a specific target product ID. Second, the attacker generates a legitimate Stripe Payment Intent using a test-mode credit card (against the site's public test key). Finally, the attacker triggers the 'asp_next_action_results' endpoint while passing 'is_live=false' and the test payment intent ID. Because the plugin uses the 'is_live' parameter to determine which secret key to use for server-side verification, it validates the test-mode intent against the plugin's test credentials and proceeds to fulfill the order for the live product.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.