Jump to content

Recommended Posts

I wish to lockout the user for (3) minutes if they get (4) wrong username/password attempts in (5) minutes.

 

Is this typically tied to a single IP using $_SERVER['REMOTE_ADDR']?

 

Is it a good ideal to also check for a given username but from any IP?  I might be wrong, but I assume the value in $_SERVER['REMOTE_ADDR'] is under the user's control.

 

Obviously, a session wouldn't be ideal as the associated cookie is under the user's control.  Do I need to use the database or is there a better way?

 

Any thoughts or advise would be appreciated.

the log in attempt is tied to the username being tried and the ip address the request came from. the $_SERVER['REMOTE_ADDR'] comes from the tcp/ip data packets the web server received and is where the response sent back out from the server will go to.

 

you need to log, in a database table, the username, ip address, and date/time of each failed attempt. you can then query this table to determine what happens on the next attempt. you can find out how may attempts there have been within x amount of time as well as find the time of the last attempt using one query.

 

the reason you tie this to the username and ip address the attempts are coming from, is so that if the legitimate user is already logged in, you don't inadvertently log him out just because some bot/hacker is making attempts to login. the goal is to limit the login attempts, not to harm a legitimate user.

 

you also need to detect if there is a flood of attempts that come from multiple ip address for the same username and impose a longer delay or trigger the use of a security question that must be answered in order to allow a any log in attempt. since the data will be in a database table, you can simply use a query to count all the recent attempts against a username to trigger this mode.

the reason you tie this to the username and ip address the attempts are coming from, is so that if the legitimate user is already logged in, you don't inadvertently log him out just because some bot/hacker is making attempts to login. the goal is to limit the login attempts, not to harm a legitimate user.

 

I didn't think of that.  Good point.  It wouldn't necessarily log the good guy out since he has a session granting him access, but just prevent him from logging in, right?

 

Seems like a good idea to query the failed log on table for a given IP and any username to block a script which is using random usernames and passwords.

 

Think it is better to put a time restriction on excessive failed attempts, or just require to pass a captcha?

Using the IP address for anything but informational purposes is silly, because there's only a very loose connection between people and IP addresses. A single person may have a large pool of different addresses (for legitimate or malicious purposes), and a single IP address may be shared by hundreds or thousands of users. There are big networks behind a single router, there are proxies, there are VPNs, there's Tor, there are botnets. An IP address really doesn't mean anything. If you think it does, you're likely to punish legitimate users while helping attackers.

 

Lockouts of any kind also lead to a major denial-of-service vulnerability: You basically allow anybody to lock out your entire userbase simply by sending a bunch of wrong passwords. A CAPTCHA is less devastating, but of course it's also much less effective. Even good CAPTCHAs can be solved very quickly if only you have enough people who do it for you (knowingly or unknowingly).

 

In addition to that, log-in limits create a false sense of security, because they make the password seem stronger than it actually is: Your articifical delays only work against online attacks. Once the attacker has obtained the raw hashes, they get the full speed of their hardware. And that's when a seemingly OK password may turn out to be extremely weak.

 

And if that wasn't bad enough, it's also very, very difficult to properly implement a log-in limit. No, it's not just a bunch of SELECTs and UPDATEs:

  • You have to worry about race conditions. If there's a gap between checking the current log-in counter and incrementing it, then every request between those two actions still gets the old counter value. So an attacker may be able to circumvent your limit and make hundreds or thousands of attempts simply by sending them in quick succession.
  • You need to take care of different attack styles: The attacker may try many passwords on few accounts, or they try few passwords on many accounts. You'll basically need complex heuristics to recognize any kind of unusual behaviour. It's not enough to just look at each individual account.
  • Those heuristics also need to be damn good. If the detection is too sensitive, you'll piss off legitimate users and introduce the risk of DoS attacks. If it's not sensitive enough, it doesn't provide enough protection.

So you'll need a lot of time and knowledge to get this right. And even then the whole approach is dubious at best.

 

I have a much better solution: good passwords. Any decent password hashed with bcrypt is more or less immune against brute-force attacks. So rather than desparately trying to make weak passwords survive a bit longer, you should help your users choose strong passwords. Tell them about the problem, point them to modern password managers (KeePass etc.), explain the concept of passphrases, maybe implement a password meter. Then you don't need any of those half-working log-in gimmicks.

 

I know that log-in checks are extremely popular, and it's the first thing that comes to our mind when we're worried about brute force attacks. But popular doesn't automatically mean good.

Edited by Jacques1
  • Like 1

If you need a log-in check to feel secure, keep it super-simple. It's not worth investing any time.

 

For example, make a per-acccount limit and a global limit (based on experience). If the per-account limit is exceeded, you display a CAPTCHA for this account; if the global limit is exceeded, you display a CAPTCHA for every account.

 

You need an atomic counter to correctly count the log-in attempts.

Sorry, I should have reviewed the December 12th related post before making my post.

 

Based on the specific objectives of the application, I still think that using IP addresses has some merit.  Not to deter the professional hacker, but some stupid hooligan who uses his home PC to lock out another individual (especially vulnerable if emails are used as usernames).  It would still need need to roll back to some more robust checking as you described under your other post.  Unfortunately, doing so goes in the opposite direction of super-simple.

I think this is what I will do.  It is strongly based on Jacques http://forums.phpfreaks.com/topic/293061-validate-username-and-password/?p=1499458 solution, however, also is modified as follows:

  1. Restrict 40 wrong passwords for given username ever (i.e. a flood of attempts over time), and must be reset by the administrator
  2. Restrict 20 wrong usernames or passwords in past hour from given IP.  Maybe eliminate since there can be multiple users behind a common proxy.
  3. Restrict 3 wrong passwords in past hour for given username from given IP
  4. Restrict 4 wrong passwords in past hour for given username from any IP

An extra column is added to the users table: login_attempts_long_term INT UNSIGNED NOT NULL DEFAULT 0
 

One additional table is required.

CREATE TABLE IF NOT EXISTS bad_logins (
username VARCHAR(50) NOT NULL,
ip CHAR(15) NOT NULL,
last_login_attempt DATETIME NOT NULL,
login_attempts INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (username, ip))
ENGINE = InnoDB;
require_once __DIR__ . '/database.php';
$database = get_database_connection();

$stmt = $database->prepare('SELECT login_attempts_long_term FROM users WHERE username=?');
$stmt->execute(array($_POST['username']));
if($stmt->fetchColumn()>40) {
    echo('The account is perminately locked because of more than 40 failed log-in attemps.  Contact the sites administrator to reset.');
}

else {
    $stmt = $database->prepare('SELECT login_attempts FROM bad_logins WHERE ip=? AND NOW() < last_login_attempt + INTERVAL 1 HOUR');
    $stmt->execute(array($_SERVER['REMOTE_ADDR']));
    if($stmt->fetchColumn()>20) {
        echo('The account is temporarily locked because of more than twenty failed log-in attemps within one hour from this IP.');
    }

    else {
        $stmt = $database->prepare('SELECT login_attempts FROM bad_logins WHERE username=? AND ip=? AND NOW() < last_login_attempt + INTERVAL 1 HOUR');
        $stmt->execute(array($_POST['username'],$_SERVER['REMOTE_ADDR']));
        if($stmt->fetchColumn()>3) {
            echo('The account is temporarily locked because of more than three failed log-in attemps within one hour for this username at this IP.');
        }

        else {
            /*
            * Increment the log-in attempts counter and fetch the new value with a single atomic operation
            * to prevent race conditions.
            *
            * If the last log-in attempt was more than 1 hour ago, the counter is reset.
            */
            $loginAttemptsCheck = $database->prepare('UPDATE users SET login_attempts = LAST_INSERT_ID(IF(NOW() > last_login_attempt + INTERVAL 1 HOUR, 1, login_attempts + 1)), last_login_attempt = NOW() WHERE username = :username ');
            $loginAttemptsCheck->execute(array('username' => $_POST['username']));

            $loginAttempts = $database->lastInsertID();

            if ($loginAttempts <= 4) {
                if(validPassword()) {
                    //Set logon session
                    //Set login_attempts and login_attempts to zero
                    $stmt = $database->prepare('UPDATE users SET login_attempts=0 WHERE username=?');
                    $stmt->execute(array($_POST['username']));
                    $stmt = $database->prepare('INSERT INTO bad_logins(username,ip,login_attempts) VALUES (?,?,0) ON DUPLICATE KEY UPDATE login_attempts=0');
                    $stmt->execute(array($_POST['username'],$_SERVER['REMOTE_ADDR']));
                }
                else {
                    echo('invalid password');
                    //Increment login_attempts
                    $stmt = $database->prepare('INSERT INTO bad_logins(username,ip,login_attempts) VALUES (?,?,1) ON DUPLICATE KEY UPDATE login_attempts=login_attempts+1');
                    $stmt->execute(array($_POST['username'],$_SERVER['REMOTE_ADDR']));
                    $stmt = $database->prepare('UPDATE users SET login_attempts_long_term=login_attempts_long_term+1 WHERE username=?');
                    $stmt->execute(array($_POST['username']));
                }
            }
            else {
                echo 'The account is temporarily locked because of more than three (actually four, but do not tell user because it gives too much information?) failed log-in attemps within one hour.';
            }
        }
    }
}

Isn't this a collection of everything you should not be doing?

 

One aspect you constantly ignore is availability. It's great that you want to prevent brute-force attacks, but this isn't the only threat. If your brute-force protection allows anybody to take down the entire site with a few requests, then it's broken. You've merely replaced one vulnerability with another one.

 

You lock an account after 40 failed attempts and force an admin to manually unlock it? Then I can terminate your entire userbase and drive your admins insane simply by repeatedly entering wrong passwords. It's like you invite everybody for a DoS attack.

 

It's also unclear to me why you still use IP-based bans. This is obviously a bad idea. Once your blacklist includes some big VPNs and proxies, thousands of people who haven't done anything will be unable to reach your site. Even current users may suddenly be blocked. Is your website so incredibly popular that people will accept this and give up their current address just for you?

 

Last but not least, all checks (except the last one) are still subject to race conditions. Like I already said, you can't just do a SELECT query to get the current value and later update the counter. All requests which happen between those two queries still benefit from the old counter value. So the checks don't even work. How many attempts a user can make only depends on the processing power of your webserver.

Isn't this a collection of everything you should not be doing?

 

Not everything, I am definitely capable of more :)

 

But I don't necessarily agree with it being the wrong approach.  Let me explain why.  My expected demographics will, no matter what you or I recommend, use week passwords.  They are not targets to sophisticated international threats, only non-sophisticated geographically local yahoos who may know them and could be commercial competitors.  DoS is important, but credibly should they compromise their data to their local competitors, albeit by their own negligence, is more important.

 

You lock an account after 40 failed attempts and force an admin to manually unlock it? Then I can terminate your entire userbase and drive your admins insane simply by repeatedly entering wrong passwords. It's like you invite everybody for a DoS attack.

Given of course that you know the userames of my entire userbase.  If my entire admin staff goes insane, I suppose I could increase the number of failed attempts, reset it after a longer duration of time, or outsource my admin to a mental institution.

 

It's also unclear to me why you still use IP-based bans. This is obviously a bad idea. Once your blacklist includes some big VPNs and proxies, thousands of people who haven't done anything will be unable to reach your site.

I see there as being two objectives for doing so.  One is blocking a degenerate from a single IP from hacking all my users.  Given my expected audience's lack of having such a big VPN or proxy, this is probably not an issue, however, considering that a sophisticated non-local threat will not target them, it probably doesn't make much sense to do so.  The second objectively is preventing Billy Bob Badguy from discovering that if he logs in three times wrong to his account, he could attempt to logon to his competitor neighbor Chip Goodguy a couple of times and screw with him.  The later is a bigger deal.

 

Last but not least, all checks (except the last one) are still subject to race conditions. Like I already said, you can't just do a SELECT query to get the current value and later update the counter. All requests which happen between those two queries still benefit from the old counter value. So the checks don't even work. How many attempts a user can make only depends on the processing power of your webserver.

In regards to race conditions, I understand there to be two risks: DoS and intrusion by bruit force, which as previously stated bruit force is more of a concern given my audiences use of weak passwords and priorities.   The super processing power will only compromise DoS since the downstream atomic counter (nice word) ultimately dictates who is given access.  Do you disagree?

Edited by NotionCommotion
This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.