How a Carding Bot Burned Through 3,000 Stolen Card Numbers on a Client's WooCommerce Checkout

· 15 min read

A client messaged me at 7am on a Tuesday: "Stripe sent me 47 emails overnight about failed payments. I also have thousands of orders I've never seen before. What is happening?"

I logged into their Stripe dashboard and the Payments tab was a wall of red. Over 3,000 failed charge attempts between midnight and 6am, almost all for exactly $1.00. A handful — 23 — had succeeded. Their WooCommerce dashboard showed 3,200 new orders in the "Failed" and "Pending payment" statuses. Every one was a guest checkout with a different name, a different card, and the same pattern: a $1.00 order for the store's cheapest digital product.

This was a card testing attack. And it had already cost the client real money before they even knew it was happening.

What card testing actually is

Card testing — also called carding — is when an attacker uses a stolen list of credit card numbers and runs them through a real checkout to see which ones are still valid. The cards come from data breaches, skimming operations, or dark web marketplaces. Buyers purchase lists of thousands of card numbers and need to sort the live ones from the dead ones before using them for larger purchases.

WooCommerce stores are a common target because checkout forms are publicly accessible, guest checkout is often enabled, and the built-in rate limiting on the checkout endpoint is disabled by default. Worse, the block-based checkout posts to the Store API (/wp-json/wc/store/v1/checkout), which a bot can hit directly without ever loading the checkout page — bypassing any frontend CAPTCHA or JavaScript-based protection. A bot can submit hundreds of $1 charges per minute. Cards that go through are confirmed valid and get used — or resold — for high-value fraud. Cards that decline get discarded.

The store owner is collateral damage. They did not do anything wrong. Their checkout was just the testing tool.

The financial damage

This is the part that surprises most store owners. Stripe does not charge its standard processing fee on declined charges — but that does not mean the attack was free.

Each authorisation attempt still hits the card network, and the network charges a small fee for every attempt regardless of outcome. At scale — 3,000 attempts in a few hours — those fractions of a penny add up. More importantly, the 23 charges that succeeded would eventually turn into chargebacks. As of June 2025, Stripe charges $15 for every dispute received, plus an additional $15 counter fee if you contest it and lose. Twenty-three chargebacks at $15 each is $345 in dispute fees alone, before refunding the charged amounts.

Then there is the indirect cost. Stripe monitors your dispute rate. Visa and Mastercard flag merchants whose dispute-to-transaction ratio exceeds 0.9%. An overnight card testing attack that pushes a small store's ratio above that threshold can trigger a fraud monitoring programme — which comes with fines and increased scrutiny that last months.

The total cost of this attack: $345 in dispute fees, hours of manual cleanup, a temporarily elevated dispute ratio, and the reputational damage of 23 customers whose stolen cards were used on this store.

What the server logs showed

I SSHed in and checked the nginx access log for POST requests to the checkout endpoint:

grep -E "POST.*(wc-ajax=checkout|store/v1/checkout)" /var/log/nginx/access.log \
  | awk '{print $1}' | sort | uniq -c | sort -rn | head -20

The attack came from over 200 different IP addresses — residential proxies, not data centre IPs. Each IP made between 5 and 30 requests. This is deliberate: spreading the requests across many IPs avoids simple IP-based rate limiting.

The timing was concentrated between 01:12 and 05:48 UTC. The requests were evenly spaced — roughly two per second — which is a clear bot signature. A human does not complete checkout forms at machine-gun pace for four hours straight.

The user agent strings were a mix of Chrome and Firefox on Windows, all legitimate-looking. The bot was using a headless browser or request library with spoofed headers.

Immediate containment

The first priority was stopping the bleeding. I blocked the attack at two levels simultaneously.

Cloudflare rate limiting

The store was already behind Cloudflare. I created a rate limiting rule targeting the checkout endpoint:

In the Cloudflare dashboard under Security > WAF > Rate limiting rules, I created a rule covering both the classic and block-based checkout endpoints. The classic checkout submits via /?wc-ajax=checkout through admin-ajax.php. The block-based checkout posts to the Store API at /wp-json/wc/store/v1/checkout. The combined expression:

(http.request.method eq "POST" and (http.request.uri.path eq "/wp-json/wc/store/v1/checkout" or http.request.uri.query contains "wc-ajax=checkout"))
  • Rate: 5 requests per minute per IP
  • Action: Block for 1 hour

This stopped the attack within minutes. Any IP making more than 5 checkout attempts per minute got blocked for an hour.

Nginx rate limiting as a fallback

Cloudflare rules are effective but they are a third-party dependency. I also added rate limiting directly in the nginx configuration so the server could protect itself:

# In the http block
limit_req_zone $binary_remote_addr zone=wc_checkout:10m rate=6r/m;

# In the server block — Store API checkout (block-based checkout)
location = /wp-json/wc/store/v1/checkout {
    limit_req zone=wc_checkout burst=3 nodelay;
    limit_req_status 429;
    try_files $uri $uri/ /index.php?$args;
}

Six requests per minute per IP, with a burst allowance of 3. Legitimate customers do not submit checkout six times in sixty seconds. Bots do. I rate-limit the Store API endpoint specifically rather than the broader /checkout/ page — the page itself is a harmless GET, and rate-limiting it would interfere with legitimate browsing.

After reloading nginx, any requests that got past Cloudflare would still hit the server-level rate limit.

Cleaning up 3,200 fraudulent orders

With the attack stopped, I needed to clean up the orders. Doing this manually in WooCommerce was not an option — clicking through 3,200 orders one at a time would take days.

WP-CLI handles this. This store was running WooCommerce with HPOS enabled, so orders live in the wp_wc_orders table rather than wp_posts. The HPOS-compatible approach uses wc_get_orders() through a WP-CLI eval:

wp eval '
$orders = wc_get_orders([
    "status" => "failed",
    "date_created" => "2026-08-11...2026-08-12",
    "limit"  => -1,
    "return" => "ids",
]);
echo count($orders) . " failed orders found.\n";
'

That returned 3,147 failed orders. I deleted them in one pass:

wp eval '
$orders = wc_get_orders([
    "status" => "failed",
    "date_created" => "2026-08-11...2026-08-12",
    "limit"  => -1,
    "return" => "ids",
]);
foreach ($orders as $id) {
    $order = wc_get_order($id);
    $order->delete(true);
}
echo count($orders) . " orders deleted.\n";
'

On stores still using the legacy wp_posts storage, wp post list --post_type=shop_order --post_status=wc-failed --format=ids | xargs wp post delete --force does the same job faster.

For the 23 orders that had succeeded, I reviewed them individually. Each one was a $1.00 charge on a card that was almost certainly stolen. I refunded each through the Stripe dashboard rather than WooCommerce — refunding through Stripe directly avoids triggering WooCommerce's stock adjustment and email notification logic for orders that were never real.

I also contacted Stripe support to flag the charges as fraudulent. Stripe will sometimes waive dispute fees for merchants who proactively report card testing attacks, though this is not guaranteed.

Prevention: what I now configure on every WooCommerce store

After the cleanup, I set up multiple layers of protection. No single measure stops card testing on its own — attackers adapt. Defence in depth is the only approach that works.

1. Enable the WooCommerce Store API rate limiter

Since WooCommerce 9.6, there is a built-in rate limiter specifically for the checkout endpoint. It is disabled by default — which is why most stores are wide open to card testing. Enable it at WooCommerce > Settings > Advanced > Features by ticking "Rate limiting Checkout block and Store API." This limits customers to 3 place-order requests per 60 seconds.

For finer control, the woocommerce_store_api_rate_limit_options filter lets you adjust the thresholds:

add_filter('woocommerce_store_api_rate_limit_options', function () {
    return [
        'enabled'       => true,
        'proxy_support' => true,
        'limit'         => 5,
        'seconds'       => 60,
    ];
});

If the store is behind Cloudflare or any reverse proxy, proxy_support must be true — otherwise all visitors share the same IP and the rate limiter blocks everyone after the first few orders.

2. Stripe Radar rules

Stripe Radar is built into every Stripe account and is the most effective first line of defence against card testing. In the Stripe dashboard under More > Radar > Rules, I added:

  • Block if :card_country: != :ip_country: — declines charges where the card's issuing country does not match the customer's IP geolocation. Card testers typically use proxied IPs that do not match the card's origin.
  • Block if :charge_attempts_per_ip_address_hourly: > 5 — blocks any IP that attempts more than 5 charges in an hour. Legitimate customers do not retry checkout five times.
  • Review if :charge_attempts_per_card_number_hourly: > 2 — flags cards used more than twice in an hour for manual review.

Note: the velocity-based rules (:charge_attempts_per_ip_address_hourly:) require Radar for Fraud Teams, which costs $0.07 per screened transaction on top of standard processing fees. The country mismatch rule works on the free tier. For stores processing any meaningful volume, the upgrade is worth it — $0.07 per transaction is nothing compared to the cost of an undetected card testing attack.

3. Enforce a minimum order total

Card testers use small amounts — typically $0.50 to $2.00 — because small charges are less likely to be noticed by cardholders and less likely to trigger bank alerts. Setting a minimum order total blocks the most common attack pattern.

In WooCommerce, there is no built-in minimum order setting, but a small snippet in the theme's functions.php handles it:

add_action('woocommerce_checkout_process', function () {
    $minimum = 5;
    if (WC()->cart->subtotal < $minimum) {
        wc_add_notice(
            sprintf('A minimum order of %s is required.', wc_price($minimum)),
            'error'
        );
    }
});

This does not stop a determined attacker who switches to $5 orders, but it stops the lazy majority who use $1 as their test amount.

4. Enable Address Verification (AVS)

AVS checks whether the billing address provided at checkout matches the address the card issuer has on file. Most card testing bots use random or fake addresses, so AVS mismatches are a strong signal.

In WooCommerce Stripe settings (WooCommerce > Settings > Payments > Stripe), ensure the "Enable Payment via Saved Cards" option is not allowing charges to bypass AVS. In Stripe Radar rules, add:

  • Block if :avs_address_line1_check: = 'fail'
  • Block if :avs_zip_check: = 'fail'

These decline charges where the address or postcode does not match the card issuer's records.

5. Require 3D Secure where possible

3D Secure (3DS) adds an authentication step — the cardholder's bank prompts them to verify their identity via an app or SMS code. A bot with a stolen card number cannot pass 3DS because it does not have access to the cardholder's phone or banking app.

Stripe supports requesting 3D Secure on all transactions. In Stripe Radar rules:

  • Request 3D Secure if :risk_level: != 'normal' — triggers 3DS for any transaction Stripe's risk engine flags as elevated.
  • Request 3D Secure if :card_country: != :ip_country: — triggers 3DS when there is a country mismatch.

For maximum protection, you can require 3DS on all transactions, but this adds friction for legitimate customers. I typically enable it for elevated-risk transactions and let normal-risk ones pass through frictionlessly.

6. Add a honeypot field to checkout

A honeypot is a hidden form field that is invisible to real customers but gets filled in by bots that process every field they find. If the field has a value on submission, the order is from a bot.

add_action('woocommerce_after_checkout_billing_form', function () {
    echo '<p class="form-row" style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">';
    echo '<label for="billing_confirm_email">Confirm email</label>';
    echo '<input type="text" name="billing_confirm_email" id="billing_confirm_email" value="" autocomplete="off" tabindex="-1">';
    echo '</p>';
});

add_action('woocommerce_after_checkout_validation', function ($data, $errors) {
    if (!empty($_POST['billing_confirm_email'])) {
        $errors->add('bot_detected', 'Order validation failed.');
    }
}, 10, 2);

This is lightweight, invisible to customers, requires no third-party service, and catches a surprising number of bots. It is not foolproof — sophisticated bots can detect honeypots — but it adds another layer that has to be bypassed.

7. Restrict guest checkout on high-risk stores

Guest checkout is the easiest entry point for card testing bots because there is no account creation barrier. Requiring account creation does not stop a determined attacker, but it adds friction that slows automated attacks and creates an auditable trail.

For stores where guest checkout is a business requirement (and it usually is — conversion rates drop when you force account creation), consider requiring it only for orders below a certain value, or only for first-time customers from specific geographies.

In WooCommerce settings: WooCommerce > Settings > Accounts & Privacy. Toggle "Allow customers to place orders without an account" based on your risk tolerance.

What I now monitor

After cleaning up this attack, I added two ongoing checks to my monitoring for this store — and every WooCommerce store I manage:

  1. Stripe failed charge rate — I set up a Stripe webhook listener that alerts me when the failed charge count in any 1-hour window exceeds 10. Card testing attacks are obvious from the spike in declined charges long before the store owner notices.

  2. WooCommerce order velocity — a simple WP-CLI cron job that counts orders created in the last hour and sends an alert if the count is abnormally high:

ORDER_COUNT=$(wp wc order list --after=$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
  --field=id --user=1 2>/dev/null | wc -l)
if [ "$ORDER_COUNT" -gt 50 ]; then
    echo "Alert: $ORDER_COUNT orders in the last hour" | mail -s "WooCommerce order spike" [email protected]
fi

The threshold depends on the store's normal volume. A store that does 20 orders a day should alert at 10 in an hour. A store doing 200 a day might set the threshold at 50.

The takeaway

Card testing is one of those attacks that hits stores that have done nothing wrong. The checkout form is doing exactly what it is supposed to do — processing payments. The problem is that there is no built-in mechanism in WooCommerce to distinguish between a customer buying something and a bot testing stolen cards.

The fix is layers. Stripe Radar catches the obvious fraud patterns. Rate limiting at Cloudflare and nginx stops the volume. Honeypots and minimum order values filter out the lazy bots. 3D Secure and AVS verification stop the rest. No single layer is enough, but together they make your checkout expensive and slow for attackers to abuse — and that is usually enough to send them somewhere else.

If you are running a WooCommerce store on Stripe and have not configured Radar rules or rate limiting, your checkout is currently an open card testing terminal. It is not a matter of whether it will be targeted. It is when.

This is one of the things I check as part of routine WooCommerce maintenance. If you would rather not find out about card testing from a Stripe alert at 7am, take a look at the maintenance plans or read more about what WooCommerce maintenance involves.

Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans Get in Touch

Get in Touch to Discuss Your Needs