Skip to content

Commit fc88a6e

Browse files
authored
Fix rate limit for failed add payment method attempts (#4491)
* Fix rate limit for failed add payment method attempts * Add the exception as arg to the delay filter
1 parent 71efac7 commit fc88a6e

File tree

4 files changed

+71
-1
lines changed

4 files changed

+71
-1
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* Fix - Validate create customer payload against required billing fields before sending to Stripe
2020
* Update - Enhanced logging system with support for all log levels and improved context handling
2121
* Fix - Set default values for custom field options
22+
* Fix - Enforce rate limiter for failed add payment method attempts
2223

2324
= 9.6.0 - 2025-07-07 =
2425
* Fix - Register Express Checkout script before use to restore buttons on “order-pay” pages

includes/class-wc-stripe-intent-controller.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1116,9 +1116,10 @@ public function create_and_confirm_setup_intent( $payment_information ) {
11161116
* @throws Exception If the AJAX request is missing the required data or if there's an error creating and confirming the setup intent.
11171117
*/
11181118
public function create_and_confirm_setup_intent_ajax() {
1119+
$wc_add_payment_method_rate_limit_id = 'add_payment_method_' . get_current_user_id();
1120+
11191121
try {
11201122
// similar rate limiter is present in WC Core, but it's executed on page submission (and not on AJAX calls).
1121-
$wc_add_payment_method_rate_limit_id = 'add_payment_method_' . get_current_user_id();
11221123
if ( WC_Rate_Limiter::retried_too_soon( $wc_add_payment_method_rate_limit_id ) ) {
11231124
throw new WC_Stripe_Exception( 'Failed to save payment method.', __( 'You cannot add a new payment method so soon after the previous one.', 'woocommerce-gateway-stripe' ) );
11241125
}
@@ -1174,6 +1175,18 @@ public function create_and_confirm_setup_intent_ajax() {
11741175
} catch ( WC_Stripe_Exception $e ) {
11751176
WC_Stripe_Logger::log( 'Failed to create and confirm setup intent. ' . $e->getMessage() );
11761177

1178+
/**
1179+
* Filter the rate limit delay after a failure adding a payment method.
1180+
*
1181+
* @since 9.7.0
1182+
*
1183+
* @param int $rate_limit_delay The rate limit delay in seconds.
1184+
* @param WC_Stripe_Exception $e The exception that occurred.
1185+
*/
1186+
$rate_limit_delay = apply_filters( 'wc_stripe_add_payment_method_on_error_rate_limit_delay', 10, $e );
1187+
1188+
WC_Rate_Limiter::set_rate_limit( $wc_add_payment_method_rate_limit_id, $rate_limit_delay );
1189+
11771190
// Send back error so it can be displayed to the customer.
11781191
wp_send_json_error(
11791192
[

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,5 +129,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
129129
* Fix - Validate create customer payload against required billing fields before sending to Stripe
130130
* Update - Enhanced logging system with support for all log levels and improved context handling
131131
* Fix - Set default values for custom field options
132+
* Fix - Enforce rate limiter for failed add payment method attempts
132133

133134
[See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).

tests/phpunit/WC_Stripe_Intent_Controller_Test.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,4 +546,59 @@ public function test_mandate_options_for_card_setup_intent_for_subscription() {
546546

547547
$this->assertEquals( 'succeeded', $result->status );
548548
}
549+
550+
/**
551+
* Test that rate limiting works after a failed attempt.
552+
*/
553+
public function test_rate_limiting_on_consecutive_failed_calls() {
554+
add_filter( 'wp_doing_ajax', '__return_true' );
555+
add_filter( 'wp_die_ajax_handler', [ $this, 'wp_ajax_halt_handler_filter' ] );
556+
557+
wp_set_current_user( 1 );
558+
$_POST['wc-stripe-payment-method'] = 'pm_test_123';
559+
$_POST['wc-stripe-payment-type'] = WC_Stripe_Payment_Methods::CARD;
560+
// First call with invalid nonce - should fail and trigger rate limiting
561+
$_POST['_ajax_nonce'] = 'invalid_nonce';
562+
563+
ob_start();
564+
$this->mock_controller->create_and_confirm_setup_intent_ajax();
565+
$output = ob_get_clean();
566+
$response = json_decode( $output, true );
567+
$this->assertFalse( $response['success'] );
568+
$this->assertArrayHasKey( 'error', $response['data'] );
569+
$this->assertEquals( 'Unable to verify your request. Please refresh the page and try again.', $response['data']['error']['message'] );
570+
571+
// Second call should fail due to rate limiting, regardless of nonce.
572+
$_POST['_ajax_nonce'] = wp_create_nonce( 'wc_stripe_create_and_confirm_setup_intent_nonce' );
573+
574+
ob_start();
575+
$this->mock_controller->create_and_confirm_setup_intent_ajax();
576+
$output = ob_get_clean();
577+
578+
$response = json_decode( $output, true );
579+
$this->assertFalse( $response['success'] );
580+
$this->assertArrayHasKey( 'error', $response['data'] );
581+
$this->assertEquals( 'You cannot add a new payment method so soon after the previous one.', $response['data']['error']['message'] );
582+
583+
remove_filter( 'wp_die_ajax_handler', [ $this, 'wp_ajax_halt_handler_filter' ] );
584+
remove_filter( 'wp_doing_ajax', '__return_true' );
585+
}
586+
587+
/**
588+
* Filter to return a custom handler for AJAX requests.
589+
*
590+
* @return callable The custom handler function.
591+
*/
592+
public function wp_ajax_halt_handler_filter() {
593+
return [ $this, 'wp_ajax_print_handler' ];
594+
}
595+
596+
/**
597+
* Custom handler function to output the message.
598+
*
599+
* @param string $message The message to print.
600+
*/
601+
public function wp_ajax_print_handler( $message ) {
602+
echo wp_kses_post( $message );
603+
}
549604
}

0 commit comments

Comments
 (0)