Add Rate Limiting to WooCommerce Checkout, Registration, and Coupon Endpoints

WooCommerce stores face automated attacks that exploit the checkout and account creation flows: credential stuffing bots cycle through leaked username/password pairs against wp-login.php, card testing bots submit hundreds of small orders to validate stolen card numbers, and coupon enumeration bots brute-force discount codes by trying systematic variations. Rate limiting — restricting the number of requests a single IP or user account can make within a time window — is the primary technical control for all three attack patterns. WordPress and WooCommerce do not include built-in rate limiting for checkout, login, or registration endpoints — protection must be added at the application layer, the web server layer (Nginx limit_req module), or a WAF layer (Cloudflare Rate Limiting). Implementing rate limiting in PHP using transients provides application-level control that works regardless of hosting environment and can apply user-specific limits (by user ID) in addition to IP-based limits. Card testing specifically targets the WooCommerce order creation endpoint — limiting new orders per IP per hour to a small number (5–10) and requiring account registration before purchasing are the most effective mitigations. CAPTCHA on checkout and registration, such as Google reCAPTCHA v3 or Cloudflare Turnstile, adds a second layer of bot friction without significant UX cost for legitimate users. Velocity checks on coupon redemption — tracking how many times a specific coupon has been tried by distinct IPs in the last hour — detect enumeration attacks without outright disabling coupons. Blocking disposable email domains at registration reduces fake account creation that precedes credential stuffing — a maintained list of disposable domains is available from the disposable-email-domains GitHub repository. The CSP and SRI post addresses the client-side security layer for checkout — rate limiting and CSP together protect both the server-side and browser-side attack surfaces. The meta box security post applies the same nonce and sanitization discipline to the order management forms that rate limiting protects on the public side. Log all rate-limit hits — IP, timestamp, endpoint, and reason — to a custom table or a log file for post-incident analysis and to adjust thresholds that are either too strict (blocking legitimate customers) or too loose (allowing attacks through).

Problem: WooCommerce stores without rate limiting on checkout, login, and coupon endpoints are vulnerable to automated card testing, credential stuffing, and coupon brute-force attacks that charge fraudulent orders, lock out real customers, and exhaust payment gateway API quotas.

Solution: Add transient-based rate limiting to WooCommerce checkout and registration hooks, restrict new orders per IP per hour, block coupon enumeration by tracking per-IP coupon attempts, and log all blocks for threshold tuning.

// wp-content/plugins/myplugin/rate-limiting.php

// Helper: increment and check a per-IP transient counter
function my_rate_check(string $action, string $ip, int $limit, int $window_sec): bool {
    $key     = 'rl_' . md5($action . $ip);
    $current = (int) get_transient($key);
    if ($current >= $limit) return false; // blocked

    if ($current === 0) {
        set_transient($key, 1, $window_sec);
    } else {
        // Increment without resetting TTL (approximation)
        set_transient($key, $current + 1, $window_sec);
    }
    return true; // allowed
}

// 1. Rate-limit new WooCommerce orders: max 5 per IP per hour
add_action('woocommerce_checkout_process', function() {
    $ip = WC_Geolocation::get_ip_address();
    if (!my_rate_check('checkout', $ip, 5, HOUR_IN_SECONDS)) {
        wc_add_notice(
            __('Too many order attempts. Please wait before trying again.', 'myplugin'),
            'error'
        );
    }
});

// 2. Rate-limit coupon application: max 10 attempts per IP per 10 minutes
add_filter('woocommerce_coupon_error', function(string $err, int $code, WC_Coupon $coupon): string {
    if (in_array($code, [WC_Coupon::E_WC_COUPON_NOT_FOUND, WC_Coupon::E_WC_COUPON_INVALID_FILTERED], true)) {
        $ip = WC_Geolocation::get_ip_address();
        if (!my_rate_check('coupon', $ip, 10, 10 * MINUTE_IN_SECONDS)) {
            // Return generic error to avoid confirming the code does not exist
            return __('Too many coupon attempts. Please try again later.', 'myplugin');
        }
    }
    return $err;
}, 10, 3);

// 3. Rate-limit account registration: max 3 new accounts per IP per hour
add_action('register_post', function(string $user_login, string $user_email, WP_Error $errors) {
    if ($errors->has_errors()) return;
    $ip = WC_Geolocation::get_ip_address();
    if (!my_rate_check('register', $ip, 3, HOUR_IN_SECONDS)) {
        $errors->add('rate_limit', __('Registration temporarily blocked. Try again later.', 'myplugin'));
    }
}, 10, 3);

NOTE: Transient-based rate limiting is not perfectly accurate under high concurrency — two simultaneous requests at the limit may both pass before either increments the counter. For high-security endpoints, use Redis with atomic INCR + EXPIRE commands via the WP_Object_Cache layer for race-condition-free counting.