WooCommerce card testing attacks: a checkout lockout plugin you can build today
· 5 min read
Card testing is straightforward: a bot submits card numbers, one after another, until a payment succeeds. The goal is not to buy anything from your store. The goal is to find a live card the attacker can then sell or use elsewhere. Your checkout is just a convenient validator.
The right first response is not a plugin. It is your payment gateway settings: AVS mismatch rejection, CVV failure handling, and velocity filters that throttle repeated attempts from the same IP. If your gateway supports those controls and you have not enabled them, do that first. For most stores, those settings end the attack.
The gap this plugin fills is the space between the gateway and WordPress. Some gateway integrations do not expose those controls. Some attacks rotate cards faster than gateway velocity filters catch. A WooCommerce-level lockout tracks failed order status changes per IP and blocks that IP from reaching checkout again. It is a second layer, not a replacement.
What one build gives you
- Tracking failed WooCommerce payment attempts per IP address using WordPress transients
- Locking out IPs that exceed the failure threshold, with a checkout error notice
- An admin page listing blocked IPs with blocked-at timestamps and one-click unblock
- Configurable failure threshold, tracking window in minutes, and lockout duration in hours
What it does not do
- AVS, CVV, or velocity filtering at the payment processor level, which are the primary defense
- Bot detection, browser fingerprinting, or CAPTCHA at checkout
- IP reputation lookups, geolocation blocking, or Cloudflare integration
- Blocking by email address, phone number, or billing address patterns
- Removing failed orders from the WooCommerce database
The prompt
Loads into the composer so you can edit it first. Nothing is built, and nothing is charged, until you send it.
Build a WordPress plugin called WooCommerce Card Testing Lockout. Plugin Name: WooCommerce Card Testing Lockout. Description: Locks out IP addresses that make repeated failed payment attempts at WooCommerce checkout. Requires WooCommerce. Single PHP file. No JavaScript. No external requests. Track failures: hook woocommerce_order_status_changed at priority 10 with 4 arguments. When $new_status equals 'failed': get $ip via $order->get_customer_ip_address(). Build transient key $key = 'wc_ctlk_' . md5($ip). Read $data = get_transient($key). If $data is false, set $data = ['n' => 0, 'since' => time()]. Get $window = (int) get_option('wc_ctlk_window', 3600). If time() minus $data['since'] exceeds $window, reset $data to ['n' => 0, 'since' => time()]. Increment $data['n']. set_transient($key, $data, 86400). Get $threshold = (int) get_option('wc_ctlk_threshold', 5). If $data['n'] >= $threshold: get $blocked = get_option('wc_ctlk_blocked', []); $blocked[$ip] = time(); update_option('wc_ctlk_blocked', $blocked, false); delete_transient($key). Block at checkout: hook woocommerce_checkout_process at priority 1. Get $ip = WC_Geolocation::get_ip_address(). Get $blocked = get_option('wc_ctlk_blocked', []). If $ip is a key in $blocked: get $duration = (int) get_option('wc_ctlk_duration', 86400). If time() minus $blocked[$ip] < $duration: wc_add_notice('Too many failed payment attempts from your connection. Please try again later or contact us for help.', 'error'). Else: unset($blocked[$ip]); update_option('wc_ctlk_blocked', $blocked, false). Settings: register_setting for wc_ctlk_threshold (sanitize_callback intval), wc_ctlk_window (sanitize intval, stores seconds), wc_ctlk_duration (sanitize intval, stores seconds). Add options page via add_options_page: page title 'Card Testing Lockout Settings', menu title 'Card Testing Lockout', capability 'manage_woocommerce', menu_slug 'wc-ctlk'. Page callback renders a form posting to options.php with settings_fields and three number inputs: wc_ctlk_threshold (label 'Failure threshold', default 5, min 1, description 'Failed payments before lockout'), wc_ctlk_window_min (label 'Tracking window (minutes)', display value = get_option(wc_ctlk_window,3600)/60, save via sanitize_callback that multiplies by 60 and calls update_option('wc_ctlk_window',...) then returns null to skip auto-save), wc_ctlk_duration_hrs (label 'Lockout duration (hours)', display value = get_option(wc_ctlk_duration,86400)/3600, save via sanitize_callback multiplying by 3600 into wc_ctlk_duration). Blocked IPs page: add_submenu_page under wc-ctlk, page title 'Blocked IPs', menu_slug 'wc-ctlk-blocked'. Page callback: get blocked list; if empty show a paragraph 'No IPs are currently blocked.'; else show a table with class widefat and columns IP Address, Blocked At, Action. Each row shows the IP, date_i18n('Y-m-d H:i:s', $blocked[$ip]), and a form posting to admin-post.php with action wc_ctlk_unblock, hidden field ip_address set to $ip, wp_nonce_field('wc_ctlk_unblock'), submit button 'Unblock'. Show an admin-notice if GET unblocked=1. Register admin_post_wc_ctlk_unblock: check_admin_referer('wc_ctlk_unblock'); get $ip = sanitize_text_field($_POST['ip_address']); get $blocked = get_option('wc_ctlk_blocked', []); unset($blocked[$ip]); update_option('wc_ctlk_blocked', $blocked, false); wp_redirect(admin_url('options-general.php?page=wc-ctlk-blocked&unblocked=1')); exit.
How card testing works in WooCommerce
A bot submits a checkout with stolen card data. WooCommerce creates an order. The payment gateway declines and marks the order failed. The bot immediately tries the next card number. Each attempt creates a failed order in your database. A busy attack leaves dozens or hundreds of failed orders before anyone notices.
The bot typically works from a list of card numbers with the same billing details. Because each attempt goes through the full checkout form, standard rate limiting on page views does not stop it. The bot is submitting a real, complete checkout.
What slows or stops the attack: gateway-level controls that reject suspicious patterns before WooCommerce even marks an order failed; Cloudflare or another WAF that detects the submission pattern; CAPTCHA that adds a human-verification step; or a plugin that counts failed orders per IP and locks the IP out of checkout after a threshold.
Gateway settings: check these before installing any plugin
Most payment gateways have three controls that stop card testing more effectively than anything in WordPress. AVS (Address Verification Service) rejects a transaction when the billing address does not match the address on file for the card. CVV rejection declines payments where the three-digit security code is wrong. Velocity filters block transactions when one IP submits more than a set number in a short window.
Where to find them depends on the gateway. Stripe has fraud protection settings in the Dashboard under Radar. Authorize.Net has Velocity Filters and AFDS settings under Account > Security Settings. Square has fraud prevention settings in the dashboard. PayPal has Fraud Management Filters in the account settings.
If your gateway plugin does not surface these controls in the WordPress admin, log in directly to the gateway dashboard. Some older WooCommerce gateway integrations pass transactions through without exposing the account's security settings in the plugin interface at all.
What the lockout plugin does
When a WooCommerce order transitions to failed status, the plugin reads the customer's IP address from the order and increments a per-IP counter stored in a transient. If the count reaches the threshold (default: five failures in sixty minutes), the IP is added to a blocked list in wp_options and the transient is cleared.
At the start of checkout processing, the plugin checks the current visitor's IP against the blocked list. If the IP is on the list and the lockout period has not expired (default: 24 hours), WooCommerce adds an error notice and stops the checkout. The visitor cannot proceed to payment. After the lockout expires, the IP is automatically removed from the list on the next checkout attempt.
An admin page under Settings shows each blocked IP, the time it was blocked, and an Unblock button. A legitimate customer who triggered the lockout can be unblocked in one click. The threshold, tracking window, and lockout duration are all configurable from the settings screen.
What the plugin cannot do
An IP lockout assumes the attacker is coming from one IP address. Coordinated attacks often rotate through hundreds of IPs. Against those, the lockout raises the cost slightly but does not stop the attack. The gateway's velocity filters or a network-level tool like Cloudflare are what stop volume attacks.
The plugin also does not remove the failed orders from your database. WooCommerce keeps every failed order by default. After an attack, you will want to clean those up. A database cleanup plugin can remove orders stuck in failed status beyond a given age.
False positives are possible on shared IPs: a hotel, a university network, or a corporate proxy where many users share one outbound address. If you see legitimate customers getting blocked, raise the threshold or use the Blocked IPs page to release them.
Questions
- A legitimate customer says they cannot check out. What do I do?
Go to Settings > Blocked IPs, find their IP address in the table, and click Unblock. They can then complete the checkout immediately. If you see real customers being blocked regularly, raise the failure threshold in Settings > Card Testing Lockout Settings.
- Will this stop a serious card testing attack?
It raises the cost for simple single-IP attacks but will not stop volume attacks that rotate through many IP addresses. For those, the right tools are gateway-level velocity filters configured in your payment processor dashboard, or a network-level WAF like Cloudflare. This plugin is a second layer, not the main defense.
- Why are there hundreds of failed orders in my WooCommerce database after an attack?
WooCommerce creates an order for every checkout attempt, including failed ones, and does not delete them automatically. After an attack you can clean them up with a database query or a cleanup plugin that removes orders stuck in failed status beyond a set age.
- Does this work with every payment gateway?
It hooks into the WooCommerce order status rather than any specific gateway, so it works with any gateway that transitions orders to failed status when a payment is declined. That covers Stripe, Authorize.Net, PayPal, Square, and most others.
The prompt
Loads into the composer so you can edit it first. Nothing is built, and nothing is charged, until you send it.
Build a WordPress plugin called WooCommerce Card Testing Lockout. Plugin Name: WooCommerce Card Testing Lockout. Description: Locks out IP addresses that make repeated failed payment attempts at WooCommerce checkout. Requires WooCommerce. Single PHP file. No JavaScript. No external requests. Track failures: hook woocommerce_order_status_changed at priority 10 with 4 arguments. When $new_status equals 'failed': get $ip via $order->get_customer_ip_address(). Build transient key $key = 'wc_ctlk_' . md5($ip). Read $data = get_transient($key). If $data is false, set $data = ['n' => 0, 'since' => time()]. Get $window = (int) get_option('wc_ctlk_window', 3600). If time() minus $data['since'] exceeds $window, reset $data to ['n' => 0, 'since' => time()]. Increment $data['n']. set_transient($key, $data, 86400). Get $threshold = (int) get_option('wc_ctlk_threshold', 5). If $data['n'] >= $threshold: get $blocked = get_option('wc_ctlk_blocked', []); $blocked[$ip] = time(); update_option('wc_ctlk_blocked', $blocked, false); delete_transient($key). Block at checkout: hook woocommerce_checkout_process at priority 1. Get $ip = WC_Geolocation::get_ip_address(). Get $blocked = get_option('wc_ctlk_blocked', []). If $ip is a key in $blocked: get $duration = (int) get_option('wc_ctlk_duration', 86400). If time() minus $blocked[$ip] < $duration: wc_add_notice('Too many failed payment attempts from your connection. Please try again later or contact us for help.', 'error'). Else: unset($blocked[$ip]); update_option('wc_ctlk_blocked', $blocked, false). Settings: register_setting for wc_ctlk_threshold (sanitize_callback intval), wc_ctlk_window (sanitize intval, stores seconds), wc_ctlk_duration (sanitize intval, stores seconds). Add options page via add_options_page: page title 'Card Testing Lockout Settings', menu title 'Card Testing Lockout', capability 'manage_woocommerce', menu_slug 'wc-ctlk'. Page callback renders a form posting to options.php with settings_fields and three number inputs: wc_ctlk_threshold (label 'Failure threshold', default 5, min 1, description 'Failed payments before lockout'), wc_ctlk_window_min (label 'Tracking window (minutes)', display value = get_option(wc_ctlk_window,3600)/60, save via sanitize_callback that multiplies by 60 and calls update_option('wc_ctlk_window',...) then returns null to skip auto-save), wc_ctlk_duration_hrs (label 'Lockout duration (hours)', display value = get_option(wc_ctlk_duration,86400)/3600, save via sanitize_callback multiplying by 3600 into wc_ctlk_duration). Blocked IPs page: add_submenu_page under wc-ctlk, page title 'Blocked IPs', menu_slug 'wc-ctlk-blocked'. Page callback: get blocked list; if empty show a paragraph 'No IPs are currently blocked.'; else show a table with class widefat and columns IP Address, Blocked At, Action. Each row shows the IP, date_i18n('Y-m-d H:i:s', $blocked[$ip]), and a form posting to admin-post.php with action wc_ctlk_unblock, hidden field ip_address set to $ip, wp_nonce_field('wc_ctlk_unblock'), submit button 'Unblock'. Show an admin-notice if GET unblocked=1. Register admin_post_wc_ctlk_unblock: check_admin_referer('wc_ctlk_unblock'); get $ip = sanitize_text_field($_POST['ip_address']); get $blocked = get_option('wc_ctlk_blocked', []); unset($blocked[$ip]); update_option('wc_ctlk_blocked', $blocked, false); wp_redirect(admin_url('options-general.php?page=wc-ctlk-blocked&unblocked=1')); exit.
Steem