How can I throttle user login attempts in PHP

2019-01-05 07:48发布

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?

12条回答
三岁会撩人
2楼-- · 2019-01-05 07:53

As per discussion above, sessions, cookies and IP addresses are not effective - all can be manipulated by the attacker.

If you want to prevent brute force attacks then the only practical solution is to base the number of attempts on the username provided, however note that this allows the attacker to DOS the site by blocking valid users from logging in.

e.g.

$valid=check_auth($_POST['USERNAME'],$_POST['PASSWD']);
$delay=get_delay($_POST['USERNAME'],$valid);

if (!$valid) {
   header("Location: login.php");
   exit;
}
...
function get_delay($username,$authenticated)
{
    $loginfile=SOME_BASE_DIR . md5($username);
    if (@filemtime($loginfile)<time()-8600) {
       // last login was never or over a day ago
       return 0;
    }
    $attempts=(integer)file_get_contents($loginfile);
    $delay=$attempts ? pow(2,$attempts) : 0;
    $next_value=$authenticated ? 0 : $attempts + 1;
    file_put_contents($loginfile, $next_value);
    sleep($delay); // NB this is done regardless if passwd valid
    // you might want to put in your own garbage collection here
 }

Note that as written, this procedure leaks security information - i.e. it will be possible for someone attacking the system to see when a user logs in (the response time for the attackers attempt will drop to 0). You might also tune the algorithm so that the delay is calculated based on the previous delay and the timestamp on the file.

HTH

C.

查看更多
做自己的国王
3楼-- · 2019-01-05 07:57

IMHO, defense against DOS attacks is better dealt with at the web server level (or maybe even in the network hardware), not in your PHP code.

查看更多
淡お忘
4楼-- · 2019-01-05 07:59

You can use sessions. Anytime the user fails a login, you increase the value storing the number of attempts. You can figure the required delay from the number of attempts, or you can set the actual time the user is allowed to try again in the session as well.

A more reliable method would be to store the attempts and new-try-time in the database for that particular ipaddress.

查看更多
Summer. ? 凉城
5楼-- · 2019-01-05 08:00

I generally create login history and login attempt tables. The attempt table would log username, password, ip address, etc. Query against the table to see if you need to delay. I would recommend blocking completely for attempts greater than 20 in a given time (an hour for example).

查看更多
我想做一个坏孩纸
6楼-- · 2019-01-05 08:04

Cookies or session-based methods are of course useless in this case. The application has to check the IP address or timestamps (or both) of previous login attempts.

An IP check can be bypassed if the attacker has more than one IP to start his/her requests from and can be troublesome if multiple users connect to your server from the same IP. In the latter case, someone failing login for several times would prevent everyone who shares the same IP from logging in with that username for a certain period of time.

A timestamp check has the same problem as above: everyone can prevent everyone else from logging in a particular account just by trying multiple times. Using a captcha instead of a long wait for the last attempt is probably a good workaround.

The only extra things the login system should prevent are race conditions on the attempt checking function. For example, in the following pseudocode

$time = get_latest_attempt_timestamp($username);
$attempts = get_latest_attempt_number($username);

if (is_valid_request($time, $attempts)) {
    do_login($username, $password);
} else {
    increment_attempt_number($username);
    display_error($attempts);
}

What happens if an attacker sends simultaneous requests to the login page? Probably all the requests would run at the same priority, and chances are that no request gets to the increment_attempt_number instruction before the others are past the 2nd line. So every request gets the same $time and $attempts value and is executed. Preventing this kind of security issues can be difficult for complex applications and involves locking and unlocking some tables/rows of the database, of course slowing the application down.

查看更多
\"骚年 ilove
7楼-- · 2019-01-05 08:04

You cannot simply prevent DoS attacks by chaining throttling down to a single IP or username. Hell, 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 user's 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.

查看更多
登录 后发表回答