Preventing Brute Force on Login

With default cake login procedures a user could try unlimited passwords to one specific account.
That means, if you write a bot that tries every possible combination (thousand times per minute!), this bot could eventually gain access to the account. To cloak it, he could use several bots on several different locations or proxies.

Some websites use the session to count the attempts and display a captcha after x failures. Or they use the IP address to block this user for a few minutes. Both is not very secure.
A user could change his session and his IP frequently (depending on the skills of the attacker of course).
Then he would still have unlimited trials.

More secure mechanisms

Just some ideas to discuss here – i didn’t yet test the ideas in real life scenarios.

I don’t like the captcha stuff. I would prefer the login timeout. Either way we need to make sure that everybody – no matter if human, bot or both – gets the same result.

Lets say, our "attacker" targets "benni123" and wants access.
He tries several combinations and suddenly he cannot login for a few minutes. No matter what he does.
How?

My idea: use the database for it. Store the information based on the record the user wants to validate against.
Every failure the database stores it with the User.user_id of "benni123" and the time-stamp.
If the amount is greater as 5 for example, the login is always aborted for this user account. The real user cannot login as well, of course. But after a few minutes everything is fine again – and he even knows somebody tried to access the account.

No bot would ever find the right key. Its just not reasonable to write a bot that only tries to access the account every few minutes. It would need hundreds of years with medium-sized passwords.

You could also display a captcha (google does it this way) after x failures. But they "could" be cracked using a specific captcha reader for your site. once this is in place, they provide not much of security anymore. Meaning: The captcha must be complicated enough for the bot but still readable for humans. reality shows that this is not easy to accomplish. Therefore I tend to use captchas only to slow too active users down (prevent spamming, message flooding, etc).

Additionally we want a php script timeout (wait x seconds to respond) after unsuccessfull attempts.
This way the user doesn’t really notice but any brute force script would need remarkably longer for a chain of attempts.

In the end we could do this:
Display a captcha after maybe 5 wrong trials. After another 5 correct captcha inputs but incorrect password inputs (important! we dont want this to happen if the catpcha is wrong – otherwise an attacker could easily shut down all accounts) shut the account down for a specific timeout. Of course, if the valid user tries to access the account in that time he needs to get a proper error message.

Implementation

Using the timeout method we extend the core auth component:
Component AuthExt:

/**
 * override auth login
 * @override
 */
function login($data = null) {
    if (!$this->loginExt($data)) {
        # security timeout?
        sleep(1); # 1 second for right now
        return $this->_loggedIn;
    }

    $model = $this->getModel();

    // do final stuff (getting roles and additional session infos, raise login counter, ...)

    return $this->_loggedIn;
}

function loginExt($data = null) {
    $this->__setDefaults();
    $this->_loggedIn = false;

    if (empty($data)) {
        $data = $this->data;
    }

    # check against database entries if user is temporary banned
    if ($this->floodProtectionActive($data)) { # TODO: implement
        return $this->_loggedIn;
    }

    if ($user = $this->identify($data)) {

        // here we can check if other reasons prevent returning true (suspended, account not active yet, ...)

        return $this->_loggedIn;
    }
    # seems like we need to log this attempt as a failure
    $this->floodProtectionUpdate($data); # TODO: implement
    
    return $this->_loggedIn;
}

Security Layers

Level 1: Javascript (Timeouts, …)

  • LOW!
  • Can easily be bypassed

Level 2: Captchas, IP/Session based login monitoring, Sleep-Timeouts

  • MEDIUM
  • Can be spoofed/hacked (with some expertise anyway)

Level 3: Database based monitoring (Errors per user and login timeouts)

  • HIGH
  • Can be used against members (blocking access for valid users)

Final words

You would need a garbage collector for the database table. The last x+1 records are more than enough (except you want to debug the information or transform them into statistics).

Using only captchas:
Assuming that captchas are still as secure as we would like them to be, this is the easiest implementation.
After x failures, an additional captcha has to be entered.
The real user would get a captcha right away, if an attacker just tried to access his account. So no delay at all.
The bot, though, should fail due to the captcha in place.

Using a script timeout:
As mentioned above, the script timeout (sleep command/function) is really helpful in order to slow down brute force attempts.
A few seconds are more than enough. Normally 100 attempts need 5 seconds, with this timeout they will need more than 200.
Assuming that the attacker doesn’t use 100 parallel connections, of course.

Using a block timeout:
The real user could be identified by cookies, specific browser fingerprint etc.
This in combination with the correct password in the first place could override the above protection mechanism. allowing the user to enter EVEN if the timeout is in place due to too many failures.
The attacker should not be able to guess the fingerprint, though.
Maybe the "browser user agent" combined with a cookie information (maybe even with the IP, although it could already have changed). Even if the attacker knew that both are used for the override, he would have too many combinations ABOVE the main problem: the correct password.
He would need to know the content of the encrypted cookie as well as the exact browser version used from the real user the last time he logged on.

Combining them:
This way it is most secure on application level. Even if a working captcha reader is found for your captchas, the attacker will not be able to brute force login.

One advantage of the database log tables:
You can easily monitor the fails and get a pretty good picture how many "brute force attacks" have actually been commited on your website.

What do you think? And is the possible "override" maybe the key for vulnerabilities again? At least for a specific target this might be plausible.

0.00 avg. rating (0% score) - 0 votes

4 Comments

  1. you still would need to solve the captcha repeatedly…
    altogether a not very likely scenario. although possible, of course.

    maybe the REAL user could override it by email verification of some sort.
    if he clicks the link in the email, the system knows which IP + session is the valid one.
    if the timeout is only a few minutes he could as well wait, of course.

  2. I have implemented a function which has to return true in order to proceed with the Auth->login(). It requires a field in User Model called "next_login_attempt" which is updated on every login attempt according to a preconfigured delay time. Thus if someone tries to login before the specified timestamp, it fails.

        /**
         * Checks if a login attempt for a specific User is happening after the 
         * configured delay between login attempts.
         * @return boolean true if after specified delay, false otherwise
         */
        protected function _allowedAttempt(){
            App::uses('CakeTime', 'Utility');
            $user=$this->User->find('first',array('conditions'=>array(
                'User.email' => $this->request->data('User.email')
            )));
            if(empty($user)) return false;
            $this->User->id = $user['User']['id'];
            if($user['User']['next_login_attempt']===null){
                $this->User->saveField('next_login_attempt', CakeTime::format('Y-m-d H:i:s',Configure::read('App.loginAttemptsDelay')));
                return true;
            }
            $nextLogin = CakeTime::fromString($user['User']['next_login_attempt']);
            $this->User->saveField('next_login_attempt', CakeTime::format('Y-m-d H:i:s',Configure::read('App.loginAttemptsDelay')));
            if($nextLogin<time()) return true;
            return false;
        }

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.