Stopping bot attacks and carding on WooCommerce: Sucuri vs Cloudflare WAF, with log detection you can run tonight
Disclosure: some links in this guide are affiliate links. If you sign up through them, I may earn a commission at no extra cost to you. How this site makes money.
Stop card testing in layers, as early as possible: a WAF in front of the store (Sucuri as a managed service, or Cloudflare if someone on your team owns the rules), rate limits on the payment endpoints at the origin, WooCommerce's own checkout rate limit, and velocity rules at the payment gateway. Rate limit by IP alone fails against testers that rotate through many addresses, so also watch whole networks and attempts from visitors who never browsed a product. Sucuri suits stores without a security person: one price per site, its team handles the WAF rules and virtual patching, malware cleanup is included, and Protected Pages can put a CAPTCHA on the add payment method page. Cloudflare gives more control and a free tier, but rate limiting on Free is one rule with a 10-second window, and Bot Fight Mode can't be skipped for payment gateway callbacks.
Card testing looks like a traffic spike at 3 a.m. that converts at almost zero. A script takes a list of stolen card numbers and tries each one against a real checkout, usually for a cheap product or through the "add payment method" page that verifies a card without placing an order. Every attempt costs you something: a PHP worker for a second or two, a gateway call your processor may bill, and a declined authorization on your merchant account's record. Enough of them and the problem stops being server load and becomes your payment processor's risk team.
Plan features and prices come from Sucuri's and Cloudflare's product pages and documentation, WooCommerce's Store API documentation and acquirer and vendor publications on card network programs, checked in October 2026. The log scanner was tested on a synthetic six-hour Nginx log with 400 normal shoppers, one IP hammering the add payment method page, 30 rotating IPs testing cards through the Store API, and payment gateway callbacks. The Nginx configuration was not run on a live server. Card network program thresholds are summarized from acquirer and vendor publications; confirm them with your acquirer.
1. The short answer
| Do it yourself at the origin | |||
|---|---|---|---|
| Who writes and tunes the rules | You | You, with Cloudflare's managed rulesets | Sucuri's team, with dashboard options for you |
| Where bad requests stop | At your web server, after the connection | At Cloudflare's edge | At Sucuri's edge |
| Rate limiting on payment endpoints | Nginx limit_req, WooCommerce checkout limit | Free: 1 rule, 10 s window. Pro: 2 rules, up to 1 min. Business: 5 rules, up to 10 min, can count by response code | Not a self-serve option in the documented security settings; handled by the team and DDoS filters |
| Challenges for humans | Plugin CAPTCHA | Turnstile (free), managed challenges | Protected Pages: CAPTCHA, password, 2FA or IP allowlist per URL |
| Malware cleanup | Yours | Not included | Unlimited cleanups included |
| Price per site | Your time | Free; Pro $20 a month billed yearly; Business $200 | Firewall from $9.99 a month; platform with cleanup from $229 a year |
| Best for | Teams with Linux skills, as the inner layer | Teams with someone who owns WAF rules | Stores without a security person |
2. Why carding is a payments problem, not just a load problem
- Your merchant account carries the attempts. Visa's VAMP program, which replaced its separate fraud and dispute programs in 2025, tracks enumeration: card testing confirmed by Visa's own detection. Acquirer and vendor publications consistently cite an enumeration ratio above 20% with more than 300,000 enumerated transactions as the trigger. Thresholds for fraud and disputes, and the per-transaction fees passed down to merchants, vary between sources and regions, so ask your acquirer which apply to you.
- Attempts can cost money even when declined. Many processors charge per authorization attempt, not per sale. Check your pricing for authorization fees before assuming a declined card is free.
- Load is real too. A checkout or payment method request runs WordPress, WooCommerce and a call to the gateway. That ties up a PHP worker for as long as the gateway takes to answer.
3. Defense in layers
No single layer is enough. IP rate limits miss testers that rotate through thousands of residential addresses. Bot detection misses real browsers driven by automation. Gateway rules act only after you've paid for the request. Together they leave little room.
4. Finding card testing in your logs
Before changing anything, find out whether it's happening. Card testing has a recognizable shape in access logs: many POSTs to the payment endpoints, from addresses that never viewed a product, often spread across one network so each IP stays under a per-IP limit. This scanner reads Nginx or Apache logs in the combined format, plain or gzipped:
#!/usr/bin/env python3
"""Find card-testing traffic in Nginx or Apache access logs (combined format) of a WooCommerce store.
Python 3.9+, standard library only. Reads plain or .gz logs; exits 1 when something is flagged, for cron.
python3 carding_scan.py /var/log/nginx/access.log /var/log/nginx/access.log.1
python3 carding_scan.py /var/log/nginx/access.log* --blocklist block.txt # one IP or CIDR per line
Behind Cloudflare or Sucuri, log the visitor's IP (real_ip / mod_remoteip), or every line shows the proxy.
"""
import argparse
import collections
import datetime
import gzip
import ipaddress
import re
import sys
LINE = re.compile(r'(?P<ip>\S+) \S+ \S+ \[(?P<t>[^\]]+)\] "(?P<m>[A-Z]+) (?P<path>\S+)[^"]*" \d{3} \S+ '
r'"[^"]*" "(?P<ua>[^"]*)"')
# Payment attempts: classic and block checkout, and saving a card in My Account (no order needed)
PAY = re.compile(r'wc-ajax=checkout|/wc/store(/v1)?/checkout|^/checkout/?(\?|$)|add-payment-method'
r'|setup_intent')
# Gateway callbacks (wc-api, webhooks) come from the gateway's IPs at volume: never count them
SKIP = re.compile(r'[?&]wc-api=|/wc-api/|/wp-json/wc/v\d/webhooks')
BROWSE = re.compile(r'^/(product|shop|cart|product-category)/|[?&]add-to-cart=')
WINDOW = datetime.timedelta(minutes=10)
def net(ip):
try:
a = ipaddress.ip_address(ip)
except ValueError: # a hostname or '-' in the log: group it on its own
return ip
return ipaddress.ip_network('%s/%d' % (ip, 24 if a.version == 4 else 48), strict=False)
def main():
ap = argparse.ArgumentParser()
ap.add_argument('logs', nargs='+')
ap.add_argument('--max-ip', type=int, default=5, help='payment attempts per IP per 10 minutes')
ap.add_argument('--max-net', type=int, default=15, help='attempts per /24 (IPv6 /48) per 10 minutes')
ap.add_argument('--blocklist', help='write flagged IPs and networks to this file')
a = ap.parse_args()
attempts = collections.defaultdict(list) # ip -> attempt times
browsed, agents, per_min = set(), collections.defaultdict(set), collections.Counter()
for path in a.logs:
opener = gzip.open if path.endswith('.gz') else open
with opener(path, 'rt', encoding='utf-8', errors='replace') as f:
for line in f:
m = LINE.match(line)
if not m or SKIP.search(m['path']):
continue
ip = m['ip']
if m['m'] == 'GET' and BROWSE.search(m['path']):
browsed.add(ip)
elif m['m'] == 'POST' and PAY.search(m['path']):
t = datetime.datetime.strptime(m['t'], '%d/%b/%Y:%H:%M:%S %z')
attempts[ip].append(t)
agents[net(ip)].add(m['ua'])
per_min[t.replace(second=0)] += 1
def peak(times): # most attempts inside any 10-minute window
times, best, lo = sorted(times), 0, 0
for hi, t in enumerate(times):
while t - times[lo] > WINDOW:
lo += 1
best = max(best, hi - lo + 1)
return best
flagged, nets = [], collections.defaultdict(list)
for ip, ts in attempts.items():
nets[net(ip)].append(ip)
p = peak(ts)
if p > a.max_ip:
flagged.append((p, ip, '%d attempts in 10 min, %s' % (
p, 'never viewed a product or cart' if ip not in browsed else 'browsed first')))
for n, ips in nets.items():
p = peak([t for ip in ips for t in attempts[ip]])
# Rotating testers keep each IP under the per-IP limit; the network gives them away
if p > a.max_net and len(ips) > 2:
flagged.append((p, str(n), '%d attempts in 10 min from %d IPs, %d user agents, %d never browsed'
% (p, len(ips), len(agents[n]), sum(i not in browsed for i in ips))))
total = sum(len(v) for v in attempts.values())
print('%d payment attempts from %d IPs; busiest minute: %s' % (
total, len(attempts), '%s (%d)' % max(per_min.items(), key=lambda kv: kv[1]) if per_min else '-'))
for p, who, why in sorted(flagged, reverse=True):
print('FLAG %-20s %s' % (who, why))
if a.blocklist:
with open(a.blocklist, 'w') as f:
f.writelines(who + '\n' for _, who, _ in flagged)
sys.exit(1 if flagged else 0)
if __name__ == '__main__':
main()
- Payment endpoints, not just the checkout page. Classic checkout posts to
?wc-ajax=checkout, the Checkout block posts to the Store API, and the add payment method page in My Account verifies a card without an order, which makes it a favorite target. - Networks, not just IPs. The /24 (IPv6 /48) view catches testers who keep every address under your per-IP threshold.
- Never-browsed attempts. Real shoppers view a product or the cart before paying. Scripts usually go straight to the endpoint.
- Gateway callbacks are excluded.
wc-apirequests come from your payment gateway's servers in volume; counting them would flag the gateway. - Status codes don't help. WooCommerce's checkout answers HTTP 200 with a failure message for a declined card, so the scanner counts attempts, not errors.
On the synthetic log:
$ python3 carding_scan.py access.log --blocklist block.txt
805 payment attempts from 427 IPs; busiest minute: 2026-10-18 05:00:00+00:00 (20)
FLAG 203.0.113.50 120 attempts in 10 min, never viewed a product or cart
FLAG 192.0.2.0/24 90 attempts in 10 min from 30 IPs, 3 user agents, 30 never browsed
$ echo $?
1
Both attacks were flagged; none of the 400 shoppers (some with three attempts after a decline) and none of the 300 gateway callbacks were. Run it from cron every 10 minutes on the current log and alert on exit code 1. Before blocking a whole /24, check who owns it: mobile carriers and corporate networks put many real customers behind one range, so a challenge is safer than a block.
5. Origin rate limits: the layer you control completely
Whatever sits in front, rate limit the payment endpoints on the server itself. This Nginx configuration limits only payment POSTs, six a minute per IP with a burst of three, and leaves every other request alone (an empty key isn't counted):
# In the http {} block: a key only for payment attempts, so every other request is unlimited
map "$request_method:$request_uri" $pay_limit_key {
default "";
"~^POST:.*(wc-ajax=checkout|/wc/store(/v1)?/checkout|add-payment-method|setup_intent)" $binary_remote_addr;
}
limit_req_zone $pay_limit_key zone=pay:10m rate=6r/m;
# In the server {} block of the store
limit_req zone=pay burst=3 nodelay;
limit_req_status 429;
WooCommerce has its own limiter as well. Under WooCommerce, Settings, Advanced, Features, the "Rate limiting Checkout block and Store API" option caps checkout POSTs at 3 per 60 seconds. The general Store API limiter is off by default (25 POST requests per 10 seconds once enabled, through the woocommerce_store_api_rate_limit_options filter).
Behind Cloudflare or Sucuri, every request reaches your server from the proxy's addresses. Without set_real_ip_from for the provider's published ranges and the right header (Nginx's real IP module, Apache's mod_remoteip), every per-IP limit counts all your shoppers as one visitor and blocks them together. WooCommerce's limiter has the same issue: its proxy_support option is off by default, and once on, it trusts forwarded headers, so only enable it when the origin accepts traffic from the proxy alone.
6. Cloudflare: strong tools, your responsibility
- Rate limiting rules. Free gives one rule counting by IP over 10 seconds with a 10-second block, enough for a blunt "more than 5 payment POSTs in 10 seconds" rule. Pro allows windows up to a minute and blocks up to an hour. Business adds counting by response code and NAT-aware counting; as noted above, WooCommerce returns 200 for declines, so response codes help less here than on a login page.
- Custom rules. 5 on Free, 20 on Pro, 100 on Business; regular expressions only from Business.
- Challenge the page, not the AJAX call. A challenge needs a browser to render it. Put the managed challenge or Turnstile on the page that loads the form (the checkout or add payment method page), not on the background POST, which can't display a challenge and simply fails for real shoppers too.
- Bot Fight Mode and payment callbacks. Cloudflare's docs state that Bot Fight Mode can't be bypassed with WAF custom rules or Page Rules, applies to the whole domain and may challenge API traffic. Your gateway's webhooks and IPN callbacks are API traffic. On Free, either keep it off or allow the gateway's IPs with IP Access rules; on paid plans, Super Bot Fight Mode supports skip rules.
- Lock the origin. If the server answers anyone who finds its IP, attackers skip Cloudflare entirely. Accept HTTP only from Cloudflare's ranges.
7. Sucuri: the managed route
- Someone else owns the rules. Sucuri's WAF includes virtual patching for known CMS and plugin vulnerabilities, layer 3, 4 and 7 DDoS mitigation and a CDN; the rules are maintained by its team rather than written by you.
- Dashboard options that matter for carding. An aggressive bot filter blocks empty, malformed and script user agents. Emergency DDoS protection turns on a JavaScript filter that blocks clients without JavaScript, except major search engines, for use during an attack. A separate option stops anonymous proxies and the top three attack countries from registering, commenting or logging in while they can still browse.
- Protected Pages. Any URL can require a password, 2FA, an allowlisted IP or a CAPTCHA, remembered by cookie for 30 days. A CAPTCHA on
/my-account/add-payment-method/blocks the cheapest card-testing route at little cost to real customers, who rarely open that page. - Offloading. Cached pages are served from Sucuri's network; Sucuri says its CDN reduces server load by 80% on average. That's a vendor figure, but the direction is right: whatever the edge blocks or caches never reaches PHP.
- Automation. The WAF has an API; the dashboard's request generator gives you the exact call, for example to feed the scanner's block list.
- Plans. The firewall alone is $9.99 a month (Basic) or $19.98 (Pro, which can load your own SSL certificate). The Website Security Platform adds malware scanning and unlimited cleanups from $229 a year.
The trade-off is control. You configure what the dashboard exposes and ask support for the rest; there's no rule language to write a checkout-specific limit yourself. For a store that doesn't have someone to write those rules, that's the point.
8. Our opinion: trade-offs
- No security person on the team: Sucuri, plus the origin limits. One price, a team that maintains the WAF, cleanup included if something gets through, and a CAPTCHA where card testers do the most damage. Add the Nginx limit and the scanner from this guide as the inner layer.
- Someone owns infrastructure: Cloudflare Pro or Business. Free is a good start, but one 10-second rate limit rule is thin for a store under active attack. You get more control than with Sucuri, and you also get the job of tuning it.
- Don't stack two proxies. Pick one WAF in front of the store. Two proxies in a row make the real client IP, caching and troubleshooting harder for little gain.
- The gateway is not optional. Turn on your gateway's fraud rules (velocity, CVC, address, 3-D Secure where it makes sense). Edge and origin limits reduce volume; only the gateway sees the card.
Related: caching the WooCommerce cart and checkout, where a WAF's caching layer needs the same exclusions, and checkout monitoring.
9. Tools
Top picks
| Tool | Role | Why it fits |
|---|---|---|
| Managed website firewall, CDN and malware cleanup | A WAF its team maintains, bot and emergency DDoS filters, Protected Pages with CAPTCHA for the add payment method page, and unlimited malware cleanups on the platform plans. Firewall from $9.99 a month; platform from $229 a year. |
Alternatives: free and open-source tools
We don't earn anything from these links.
- Cloudflare: free plan with a WAF, one rate limiting rule and Turnstile; Pro and Business add rules and bot controls.
- Nginx limit_req: origin rate limiting, as configured above.
- WooCommerce Store API rate limiting: the built-in checkout limiter and its options.
- Fail2ban: turns log patterns into firewall bans if you'd rather not run the scanner from cron.
10. Pitfalls
- Rate limiting the proxy's IP. Behind a WAF without real IP configuration, a per-IP limit blocks all your customers at once.
- An open origin. A WAF only protects traffic that goes through it. Firewall the origin to the provider's ranges.
- Challenging AJAX endpoints. A challenge on the payment POST breaks checkout for everyone. Challenge the page.
- Blocking the gateway. Bot filters and rate limits that catch
wc-apicallbacks leave orders stuck in pending. Allow the gateway's IPs and test a payment after every rule change. - Counting errors instead of attempts. Declines return HTTP 200. Count POSTs to the payment endpoints.
- Forgetting add payment method. It verifies cards without an order and is often left wide open. Protect it, or disable saved cards if you don't use them.
- Cleaning up and moving on. After an attack, check the gateway for authorizations to void and tell your processor before their monitoring tells you.
Card testing costs a store PHP capacity, gateway fees and standing with its processor. Stop it in layers: a WAF at the edge, rate limits on the payment endpoints at the origin, WooCommerce's checkout limiter, and fraud rules at the gateway. Run the log scanner to see what's already happening. Choose Sucuri when you want the WAF managed for you, with a CAPTCHA on the add payment method page; choose Cloudflare when someone on your team will own the rules, and keep payment callbacks out of reach of its bot filters.