Throttle User Login Attempts in PHP

Throttle logins with PHP

The Problem: Brute Force Attack

If you have ever ran a successful user based website where user’s have Accounts with a username and password, you may have been the target of a Brute Force Attack. Where some one, usually with the aide of a bot tries to crack user passwords and gain access to there account.

Preventing large numbers of rapid-fire successive login attempts (ie. the brute force attack) really isn’t that difficult. But preventing it right isn’t as easy as it seems.

The Solution: 3 Choices effective against brute-force attacks

  • Present a CAPTCHA after X amount of failed login attempts (annoying as hell and sometimes ineffective as Captcha cracking technology has advanced so much in the past few years)
  • Locking accounts and requiring email verification after X amount of failed login attempts (this is a DoS attack waiting to happen)
  • Login Throttling: Setting a time delay between attempts after X failed login attempts (DoS attacks are still possible, but at least they are far less likely and a lot more complicated to pull off).

Login Throttling Method

I believe Login Throttling is the best solution, below is a simple example in PHP of how you might implement it into a user login system by storing the Failed login attempts to a MySQL Database.

The idea is simple, you cannot simply prevent rapid-fire login attempts by throttling down based on a single IP or username as the attacker can use multiple IPs and multiple user accounts as a way of bypassing your throttling efforts.

Instead, you should be tracking all failed login attempts across the site and associating them to a timeframe instead of an IP or account.

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 have trouble remembering their password.

10 failed attempts = 1 second delay
20 failed attempts = 2 seconds delay
30 failed attempts = Show Captcha

So we create a MySQL table named failed_logins to store the failed login attempts…

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 below…

# 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; 

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 user’s to use a captcha until the number of failed attempts over the given time period is less than the threshold.

<?PHP
// array of throttling
$throttle = array(10 => 1, 20 => 2, 30 => 'captcha');

// 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 Captcha 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.

This is more of a technique than a drop in code. You can take this code and expand/modify it to meet the needs of your system.

* Portions of the code in this article are credited to Corey Ballou aka cballou on StackOverflow

October 28 2012

Written by

Web-Developer from Central Florida. This blog is my outlet to what I am into at the time. PHP, Javascript, MySQL, OO Practices, Performance, Cool Software and Services, etc... Please subscribe to the RSS feed