securitythrottlinghoneypot

How can I throttle user login attempts in PHP


I was just reading this post The definitive guide to form-based website authentication on Preventing Rapid-Fire Login Attempts.

Best practice #1: A short time delay that increases with the number of failed attempts, like:

1 failed attempt = no delay
2 failed attempts = 2 sec delay
3 failed attempts = 4 sec delay
4 failed attempts = 8 sec delay
5 failed attempts = 16 sec delay
etc.

DoS attacking this scheme would be very impractical, but on the other hand, potentially devastating, since the delay increases exponentially.

I am curious how I could implement something like this for my login system in PHP?


Solution

  • You cannot simply prevent DoS attacks by chaining throttling down to a single IP or username. You can't even really prevent rapid-fire login attempts using this method.

    Why? Because the attack can span multiple IPs and user accounts for the sake of bypassing your throttling attempts.

    I have seen posted elsewhere that ideally you should be tracking all failed login attempts across the site and associating them to a timestamp, perhaps:

    CREATE TABLE failed_logins (
        id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
        username VARCHAR(16) NOT NULL,
        ip_address INT(11) UNSIGNED NOT NULL,
        attempted DATETIME NOT NULL,
        INDEX `attempted_idx` (`attempted`)
    ) engine=InnoDB charset=UTF8;
    

    A quick note on the ip_address field: You can store the data and retrieve the data, respectively, with INET_ATON() and INET_NTOA() which essentially equate to converting an ip address to and from an unsigned integer.

    # example of insertion
    INSERT INTO failed_logins SET username = 'example', ip_address = INET_ATON('192.168.0.1'), attempted = CURRENT_TIMESTAMP;
    # example of selection
    SELECT id, username, INET_NTOA(ip_address) AS ip_address, attempted;
    

    Decide on certain delay thresholds based on the overall number of failed logins in a given amount of time (15 minutes in this example). You should base this on statistical data pulled from your failed_logins table as it will change over time based on the number of users and how many of them can recall (and type) their password.


    > 10 failed attempts = 1 second
    > 20 failed attempts = 2 seconds
    > 30 failed attempts = reCaptcha
    

    Query the table on every failed login attempt to find the number of failed logins for a given period of time, say 15 minutes:


    SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute);
    

    If the number of attempts over the given period of time is over your limit, either enforce throttling or force all users to use a captcha (i.e. reCaptcha) until the number of failed attempts over the given time period is less than the threshold.

    // array of throttling
    $throttle = array(10 => 1, 20 => 2, 30 => 'recaptcha');
    
    // retrieve the latest failed login attempts
    $sql = 'SELECT MAX(attempted) AS attempted FROM failed_logins';
    $result = mysql_query($sql);
    if (mysql_affected_rows($result) > 0) {
        $row = mysql_fetch_assoc($result);
    
        $latest_attempt = (int) date('U', strtotime($row['attempted']));
    
        // get the number of failed attempts
        $sql = 'SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute)';
        $result = mysql_query($sql);
        if (mysql_affected_rows($result) > 0) {
            // get the returned row
            $row = mysql_fetch_assoc($result);
            $failed_attempts = (int) $row['failed'];
    
            // assume the number of failed attempts was stored in $failed_attempts
            krsort($throttle);
            foreach ($throttle as $attempts => $delay) {
                if ($failed_attempts > $attempts) {
                    // we need to throttle based on delay
                    if (is_numeric($delay)) {
                        $remaining_delay = time() - $latest_attempt - $delay;
                        // output remaining delay
                        echo 'You must wait ' . $remaining_delay . ' seconds before your next login attempt';
                    } else {
                        // code to display recaptcha on login form goes here
                    }
                    break;
                }
            }        
        }
    }
    

    Using reCaptcha at a certain threshold would ensure that an attack from multiple fronts would be stopped and normal site users would not experience a significant delay for legitimate failed login attempts.