Incident Response

Protecting WordPress from Credential Stuffing Attacks

Credential stuffing uses stolen credentials from data breaches to access accounts. Learn how to detect and prevent these automated attacks on your WordPress site.

S
Sarah Chen
8 min read
1,454 views
Preventing credential stuffing attacks on WordPress

Credential stuffing attacks use massive lists of stolen username/password combinations from data breaches to attempt logins across many websites. Unlike brute force attacks that guess passwords, credential stuffing uses real credentials—making them harder to detect and more likely to succeed.

How Credential Stuffing Works

  1. Attackers obtain stolen credentials from data breaches
  2. Automated tools test credentials across many sites
  3. Password reuse means some credentials work
  4. Compromised accounts are exploited or sold

Why WordPress Sites Are Targeted

  • Predictable login URLs (/wp-login.php, /wp-admin)
  • Large number of WordPress sites to attack
  • Often have valuable user data or e-commerce
  • Administrative access enables further attacks

Detection Methods

Monitor Login Patterns

// Detect credential stuffing patterns
function detect_credential_stuffing($username) {
    global $wpdb;

    $ip = wpfs_get_client_ip();
    $window = 5 * MINUTE_IN_SECONDS;

    // Count recent login attempts from this IP
    $attempts = $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(DISTINCT username_tried)
         FROM {$wpdb->prefix}login_attempts
         WHERE ip_address = %s
         AND attempt_time > DATE_SUB(NOW(), INTERVAL %d SECOND)",
        $ip, $window
    ));

    // Multiple different usernames from same IP = credential stuffing
    if ($attempts > 5) {
        // Block this IP
        block_ip_address($ip, 'Credential stuffing detected');

        // Alert administrators
        send_security_alert('Credential stuffing attack', array(
            'ip' => $ip,
            'usernames_tried' => $attempts,
            'window' => '5 minutes'
        ));

        return true;
    }

    // Log this attempt
    $wpdb->insert(
        $wpdb->prefix . 'login_attempts',
        array(
            'ip_address' => $ip,
            'username_tried' => $username,
            'attempt_time' => current_time('mysql')
        )
    );

    return false;
}
add_action('wp_login_failed', 'detect_credential_stuffing');

Identify Bot Behavior

// Detect automated login attempts
function detect_automated_logins() {
    // Check for missing or suspicious headers
    $indicators = 0;

    // No referer from own site
    $referer = $_SERVER['HTTP_REFERER'] ?? '';
    if (empty($referer) || strpos($referer, site_url()) === false) {
        $indicators++;
    }

    // Missing common browser headers
    if (empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
        $indicators++;
    }

    // Suspicious user agent
    $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
    if (empty($ua) || strlen($ua) < 20 || preg_match('/curl|wget|python|php/i', $ua)) {
        $indicators++;
    }

    // Very fast form submission (< 2 seconds)
    if (isset($_POST['login_timestamp'])) {
        $elapsed = time() - intval($_POST['login_timestamp']);
        if ($elapsed < 2) {
            $indicators++;
        }
    }

    return $indicators >= 2;
}

Prevention Strategies

1. Rate Limiting

// Progressive rate limiting
function progressive_rate_limit($user, $username, $password) {
    $ip = wpfs_get_client_ip();
    $attempts = get_failed_attempts($ip);

    // Exponential backoff
    $delays = array(
        0 => 0,      // First attempt: no delay
        3 => 5,      // After 3: 5 second delay
        5 => 30,     // After 5: 30 second delay
        10 => 300,   // After 10: 5 minute delay
        20 => 3600   // After 20: 1 hour delay
    );

    $required_delay = 0;
    foreach ($delays as $threshold => $delay) {
        if ($attempts >= $threshold) {
            $required_delay = $delay;
        }
    }

    $last_attempt = get_last_attempt_time($ip);
    $elapsed = time() - $last_attempt;

    if ($elapsed < $required_delay) {
        $wait = $required_delay - $elapsed;
        return new WP_Error(
            'rate_limited',
            sprintf('Too many attempts. Please wait %d seconds.', $wait)
        );
    }

    return $user;
}
add_filter('authenticate', 'progressive_rate_limit', 10, 3);

2. CAPTCHA After Failures

// Show CAPTCHA after failed attempts
function show_captcha_after_failures() {
    $ip = wpfs_get_client_ip();
    $failures = get_failed_attempts($ip);

    if ($failures >= 3) {
        // Render CAPTCHA
        echo '';
    }
}
add_action('login_form', 'show_captcha_after_failures');

3. Breach Detection

// Check password against known breaches
function check_password_breach($user, $password) {
    if (is_wp_error($user)) {
        return $user;
    }

    // Hash the password for API check (k-anonymity)
    $sha1 = strtoupper(sha1($password));
    $prefix = substr($sha1, 0, 5);
    $suffix = substr($sha1, 5);

    // Check Have I Been Pwned API
    $response = wp_remote_get(
        "https://api.pwnedpasswords.com/range/{$prefix}",
        array('timeout' => 5)
    );

    if (!is_wp_error($response)) {
        $body = wp_remote_retrieve_body($response);
        $hashes = explode("
", $body);

        foreach ($hashes as $hash) {
            list($hash_suffix, $count) = explode(':', trim($hash));
            if ($hash_suffix === $suffix) {
                // Password found in breach
                flag_user_for_password_change($user->ID);
                break;
            }
        }
    }

    return $user;
}
add_filter('wp_authenticate_user', 'check_password_breach', 100, 2);

4. Two-Factor Authentication

The most effective defense—even stolen credentials are useless without the second factor.

Monitoring Dashboard

// Track credential stuffing metrics
function get_stuffing_metrics() {
    global $wpdb;

    return array(
        'unique_ips_today' => $wpdb->get_var(
            "SELECT COUNT(DISTINCT ip_address)
             FROM {$wpdb->prefix}login_attempts
             WHERE attempt_time > DATE_SUB(NOW(), INTERVAL 1 DAY)"
        ),
        'blocked_ips' => $wpdb->get_var(
            "SELECT COUNT(*) FROM {$wpdb->prefix}blocked_ips
             WHERE reason = 'Credential stuffing detected'"
        ),
        'affected_users' => $wpdb->get_var(
            "SELECT COUNT(DISTINCT username_tried)
             FROM {$wpdb->prefix}login_attempts
             WHERE success = 0 AND attempt_time > DATE_SUB(NOW(), INTERVAL 1 DAY)"
        )
    );
}

Conclusion

Credential stuffing is a growing threat that exploits password reuse. Implement rate limiting, CAPTCHA challenges, breach detection, and strongly encourage two-factor authentication to protect your WordPress users.

Share:
S
Written by Sarah Chen

WP Folder Shield Team

Related Articles

Google Penalty from SEO Spam? How to Recover Your Search Rankings
Google Penalty from SEO Spam? How to Recover Your Search Rankings

Has your WordPress site been penalized by Google due to SEO spam injection? Learn how to identify...

January 16, 2026
Google Says "This Site May Be Hacked" - How to Fix It and Recover Rankings
Google Says "This Site May Be Hacked" - How to Fix It and Recover Rankings

Seeing the dreaded "This site may be hacked" warning in Google search results? Learn exactly what...

January 3, 2026
Protecting WordPress from SQL Injection Attacks
Protecting WordPress from SQL Injection Attacks

SQL injection remains one of the most dangerous web application vulnerabilities. Learn how to...

December 12, 2025

Ready to Secure Your WordPress Site?

Get complete protection with WP Folder Shield.

Get Started