Skip to content

Instantly share code, notes, and snippets.

@milanchymcak
Last active May 29, 2022 22:21
Show Gist options
  • Save milanchymcak/674f3ca9c13c1187999dde219096b918 to your computer and use it in GitHub Desktop.
Save milanchymcak/674f3ca9c13c1187999dde219096b918 to your computer and use it in GitHub Desktop.
Limit WordPress login attempts - Protect your website from brute force attacks

Hook for authentication protection

/*
 * Hook for authentication for login through transients options (stored in 'wp_options' table)
 * This hook passes three parameters: $user, $username and $password.
 * In order to generate an error on login, you will need to return a WP_Error object
 * Currently it will throw an error if we have >= 2 attempts
 * 
 * @link https://developer.wordpress.org/reference/hooks/authenticate/
 */
add_filter('authenticate', function($user, $username, $pass) {

    /** Check if we have already transient for current login session */
    if(get_transient('wp_login_attempts_'.get_real_IP())) {

        /** 
         * Fallback for deleting transients (and their timeout_ parts)
         * If WordPress installation has disabled WP Cron, then there is no way to delete transients
         * Suppress error messages for delete_transient
         */
        if(get_transient('timeout_wp_login_attempts_'.get_real_IP()) && time() >= get_transient('timeout_wp_login_attempts_'.get_real_IP())) {
            @delete_transient('wp_login_attempts_'.get_real_IP());
            @delete_transient('timeout_wp_login_attempts_'.get_real_IP()); 
            return $user;
        }

        /** Count number of attempts by visitor */
        $login_attempts = get_transient('wp_login_attempts_'.get_real_IP());

        /** return a WP_Error object if >= 2 attempts */
        if(isset($login_attempts) && is_numeric($login_attempts) && $login_attempts >= 2) {
            return new WP_Error('too_many_tried',  sprintf( __( '<strong>ERROR</strong>: You have reached authentication limit. Attempts: %1$s.'), $login_attempts));
        }
    }

    return $user;

}, 30, 3); 

Count the number of failed logins

/*
 * Hook that fires after user login has failed
 * Increase count of failed attempts for current IP address and generate/update transient option
 * 
 * @link https://developer.wordpress.org/reference/hooks/wp_login_failed/
 */
add_action('wp_login_failed', function($username) {

    if(get_transient('wp_login_attempts_'.get_real_IP())) {
        $login_attempts = get_transient('wp_login_attempts_'.get_real_IP());
        $login_attempts++;
        set_transient('wp_login_attempts_'.get_real_IP(), $login_attempts , 600);
    } else {
        set_transient('wp_login_attempts_'.get_real_IP(), 1, 600);
    }

    return $username;

}, 10, 1);

Get the real IP address of the attacker

/*
 * The get_real_IP() function will get us the attacker IP through safe 'REMOTE_ADDR'
 * Other methods such as 'HTTP_CLIENT_IP' can be easily faked/or possess security risk
 * Attacker can still spoof 'REMOTE_ADDR' but it should still serve the same purpose as we wanted
 * 
 * return real IP or 0, which can be still used as universal blocking transient
 */
if(!function_exists('get_real_IP')) {
    function get_real_IP() {
        $IP = $_SERVER['REMOTE_ADDR'];
        if(filter_var($IP, FILTER_VALIDATE_IP)) return $IP;
        return 0;
    }
}

(Optional) Suppress the other error messages

/*
 * Hook that filters the error messages displayed above the login form
 * Suppress the error message such as The username <username> is not registered on this site
 * We added a custom error message for all login errors except login attempts
 * More login errors can be customized through WP_Error::get_error_codes()
 * 
 * @link https://developer.wordpress.org/reference/hooks/login_errors/
 */
add_filter('login_errors', function($error) {

    /** Return the login attempts the error message if visitor reached required number of failed login attempts */
    if(get_transient('wp_login_attempts_'.get_real_IP()) && str_contains($error, 'You have reached authentication limit')) return $error;

    /** Our custom message for login error */
    return 'Your IP: ' . get_real_IP() . ' was logged.';

});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment