What are card testing attacks, and how does it affect the Store API?
Card testing attacks occur when a bad actor utilizes credit card information and makes small purchases to test the usability of the card. If the small transactions go through, the attacker is able to continue to use the card information fraudulently.
Recently we have seen card attacks specifically targeting the Store API alone, even without the use of the Checkout block. However, there are two important tools that can help protect against card attacks.
Tools to Mitigate Card Testing Attacks
Rate limiting, and Captcha are two effective ways to mitigate card testing attacks when utilizing the Store API.
Rate Limiting
Since the Store API ships with rate limiting built-in, you are able to slow the attacks immediately, however, not prevent them fully.
The Store API ships with rate limiting built-in, but itās disabled by default. You can enable it using the woocommerce_store_api_rate_limit_options
filter, as described in the Rate Limiting for Store API endpoints page.
Starting with WooCommerce 9.6, we will ship with the ability to enable far stricter rules, exclusively for the Place order endpoint (POST /wc/store/v1/checkout
), and directly from the UI.
Note: If your store is behind a proxy, enable the proxy mode so you don’t block genuine requests.
Enable Captcha
The second method to prevent those attacks is by enabling a Captcha, which is what the majority of stores have in place. Unfortunately, we discovered in some cases this didn’t completely stop the attacks either. When our team audited those plugins, we found that they were not protecting the Checkout Block/Store API. As a result, invalid Captcha checks were being bypassed for checkouts in that environment.
To help expedite a resolution for the invalid captcha gap in the most common Captcha plugins, our team patched the following:
Plugin name | Patched version | Release date |
Google reCaptcha by Koala Apps on woocommerce.com | 1.4.1+ | 13 Dec, 2024 |
reCaptcha by I13 solutions on woocommerce.com | 2.57+ | 13 Dec, 2024 |
reCaptcha by RelyWP on wordpress.org | 1.4.0+ | 17 Dec, 2024 |
Cloudflare Turnstile by RelyWP on wordpress.org | 1.28.0+ | 17 Dec, 2024 |
3rd Party Captcha and Fraud Protection Plugins
If youāre a developer of a Captcha or fraud protection plugin, make sure your solution is hooking into the Store API and integrating with the Checkout block.
To render your captcha before Checkoutās block Place order button, you can use the following code:
<?php
add_filter(
'render_block_woocommerce/checkout-actions-block',
function( $block_content ) {
ob_start();
?>
<div class="my-captcha-element" data-sitekey="<?php echo esc_attr( get_option( 'plugin_captcha_sitekey' ) ); ?>">
</div>
<?php
echo $block_content;
$block_content = ob_get_contents();
ob_end_clean();
return $block_content;
},
999,
1
);
?>
Replace woocommerce/checkout-actions-block
with whatever block you want to render before or after. Once rendered, you can use some JavaScript to hydrate and attach that element to the Captcha SDK.
We will use Turnstile as an example here, but a similar implementation applies to reCaptcha:
/* Woo Checkout Block */
if ( wp && wp.data ) {
var unsubscribe = wp.data.subscribe( function () {
const turnstileItem = document.querySelector(".my-captcha-element");
if ( turnstile && turnstileItem ) {
turnstile.render( turnstileItem, {
sitekey: turnstileItem.dataset.sitekey,
callback: function( data ) {
wp.data
.dispatch("wc/store/checkout")
.__internalSetExtensionData("plugin-namespace-turnstile", {
token: data,
});
},
});
unsubscribe();
}
}, "wc/store/cart" );
}
Note: The example above uses a current internal method __internalSetExtensionData
, we will notify developers if anything around it changes.
With PHP, you need to ensure the token is passed and is valid. This can be a common oversight that results in lack of protection. We suggest hooking your checks early on, specifically at the authentication step:
add_filter( 'rest_authentication_errors', 'plugin_check_turnstile_token' );
function cfturnstile_store_api_checkout_check( $result ) {
// Skip if this is not a POST request.
if ( isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] !== 'POST' ) {
// Always return the result or an error, never a boolean. This ensures other checks aren't thrown away like rate limiting or authentication.
return $result;
}
// Skip if this is not the checkout endpoint.
if ( ! preg_match( '#/wc/store(?:/v\d+)?/checkout#', $GLOBALS['wp']->query_vars['rest_route'] ) ) {
return $result;
}
// get request body
$request_body = json_decode( \WP_REST_Server::get_raw_data(), true );
if ( isset( $request_body['payment_method'] ) ) {
$chosen_payment_method = sanitize_text_field( $request_body['payment_method'] );
// Provide ability to short circuit the check to allow express payments or hosted checkouts to bypass the check.
$selected_payment_methods = apply_filters( 'plugin_payment_methods_to_skip', array('woocommerce_payments' ) );
if( is_array( $selected_payment_methods ) ) {
if ( in_array( $chosen_payment_method, $selected_payment_methods, true ) ) {
return $result;
}
}
}
$extensions = $request_body['extensions'];
if ( empty( $extensions ) ) {
return new WP_Error( 'challenge_failed', 'Captcha challenge failed' );
}
$value = $extensions['plugin-namespace-turnstile'];
if ( empty( $value ) ) {
return new WP_Error( 'challenge_failed', 'Captcha challenge failed' );
}
$token = $value['token'];
$check = my_token_check_function( $token );
$success = $check['success'];
if( $success != true ) {
return new WP_Error( 'challenge_failed', 'Captcha challenge failed' );
}
return $result;
}
Join the Conversation
We are always open to feedback and hearing your experiences, solutions, and concerns in the WooCommerce Community Slack, and GitHub.
Leave a Reply