Card Testing Attacks and the Store API

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 namePatched versionRelease date
Google reCaptcha by Koala Apps on woocommerce.com1.4.1+13 Dec, 2024
reCaptcha by I13 solutions on woocommerce.com2.57+13 Dec, 2024
reCaptcha by RelyWP on wordpress.org1.4.0+17 Dec, 2024
Cloudflare Turnstile by RelyWP on wordpress.org1.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.


Keep yourself in the loop!

This field is hidden when viewing the form
This field is hidden when viewing the form
This field is hidden when viewing the form


4 responses to “Card Testing Attacks and the Store API”

  1. Is this endpoint used by the classic shortcode checkout.

    If not is the endpoint disabled by default, or can it be disabled?

    1. Nadir Seghir Avatar
      Nadir Seghir

      Hey Ian! Checkout block uses this endpoint but isnā€™t the only user, external checkout like WooPayments and express payment buttons use it as well, disabling it would break those integrations + any other services that might use it.

  2. It’s nice to see this issue getting some attention, because as soon as the Store API was included in core (even for sites that were still on the legacy checkout, not the block checkout), I had a huge uptick in sites getting hit with card testing. And a captcha was not an option for a few use-cases (custom checkout flows). Does anyone actually want a captcha on their checkout either?

    Unfortunately, the above solution still doesn’t address the most common scenario of fraud checkouts and card-testing. The current Woo rate-limiter works based off user_id (if you’re signed in) or IP Address (if you’re not), but that doesn’t account for the scenario that most sites getting hit with card-testing face: rotating IPs. I had sites getting hit with 100 checkouts/minute, all from different IPs.

    The barrier to entry for outfits running card-testing operations with rotating IPs is low enough that most outfits do that now, and the Store API endpoints are uniform and public enough that it’s easy to target. Rotating IPs negate the Woo rate-limiter completely. Every checkout will be not-logged-in and a new IP address. At some point (this point), a “legit” checkout is indistinguishable from a “fraud” checkout, and there’s not enough context in one HTTP request to determine the difference. They need to be viewed in aggregate.

    The main thing that is always Capital T True in card-testing is a very high volume of failed orders. That is a far more reliable data point to monitor, because it’s too easy to block a lot of false positives by IP address, rate of requests, user_id, etc.

    I spent so much time playing whack-a-mole with card-testing on our sites that I ended up developing a solution for all our sites that does not require a captcha at all, doesn’t customize the checkout flow at all, real customers will never know it exists, and bots get locked out as soon as they start testing. After all, you can’t stop people from card-testing; you can only make it not worth their time to keep targeting you. We haven’t had a single issue with card-testing on any store since.

    Trying to block 100% of card-testing requires overly complex solutions that are very disruptive to normal customers. But being OK with blocking 99.9% of card-testing allows a simple solution that doesn’t affect real customers at all.

    1. Nadir Seghir Avatar
      Nadir Seghir

      Thank you Tobin! Your comments several years ago helped us prioritize thus further. I always welcome this great feedback from someone in the trenches šŸ˜€

      And a captcha was not an option for a few use-cases (custom checkout flows). Does anyone actually want a captcha on their checkout either?

      Captcha is not fun, most captcha solutions right now are silence ones (hidden unless they suspect the user). External Checkout flows is the main problem we’re trying to solve in a central way. For now, captcha plugins should (and so do) include a bypass for certain payment methods (that comes from an external Checkout, like wallets).

      Unfortunately, the above solution still doesnā€™t address the most common scenario of fraud checkouts and card-testing. The current Woo rate-limiter works based off user_id (if youā€™re signed in) or IP Address (if youā€™re not), but that doesnā€™t account for the scenario that most sites getting hit with card-testing face: rotating IPs. I had sites getting hit with 100 checkouts/minute, all from different IPs.

      Yes, rate limiting is a tool, though I think we should make it more extensible so that you can provide a different trigger (fingerprints instead of IPs for example), I created an issue for that.

      At some point (this point), a ā€œlegitā€ checkout is indistinguishable from a ā€œfraudā€ checkout, and thereā€™s not enough context in one HTTP request to determine the difference. They need to be viewed in aggregate.

      What are kind of triggers you think can be applied publicly?

      I’d also love to understand how we can make Store API/WooCommerce in general better as a self-hosted solution. We can’t bake in fraud tools, because fraud detection relies on trying to be one step ahead of attackers, if our detection system is public, then we’re giving out the solution to attackers.

      The eventual goal I have is that the infrastructure to challenge, block, unblock, and bypass challenges is part of core, and external services are the one that hooks to judge a request. Whatever system you developed would help us plan for that future.

      If you don’t feel comfortable sharing this publicly, you can reach out to me via email or WooCommerce slack or twitter.

Leave a Reply

Your email address will not be published. Required fields are marked *