17

I am trying to write a script to prevent brute-force login attempts in a website I'm building. The logic goes something like this:

  1. User sends login information.
  2. Check if username and password is correct
    • If Yes, let them in.
    • If No, record a failed attempt in the database. Check if there's too many fails within a given timeframe (eg: 5 in 5 minutes):
      • If Yes, then pause execution for 10 seconds: sleep(10), then report a login failure to the user.
      • Report a login failure to the user immediately

Explaining this to a co-worker, I was asked how this would help if a hacker sent, say, 1000 requests in one second. Would the first 5 would return immediately, and then the remaining 995 all take only 10 seconds?

I have a sneaking suspicion that I don't fully understand how HTTP works - is that situation above even possible, or is there a limit to the number of concurrent requests that a server will handle from one client?

Would a better solution be to have an increasing sleep time?

sleep($numRequestsInLast5Minutes - 5)

So the first 5 would be fast, and then every subsequent one would increase the sleep.

nickf
  • 537,072
  • 198
  • 649
  • 721
  • 1
    Instead of introducing a sleep, why not introduce a captcha after a maximum allowed failed login attempts? – foxybagga Jan 07 '13 at 19:50

5 Answers5

16

The problem is the balance between user accessibility and attacker model.

First Solution

If not password correct for a certain number of time:
    block the user
    send a reset link to the user

User: could be blocked, and they don't like to reset
Attacker: blocked all users by trying to authenticate to all users (especially if all logins are publicly available)

Second solution

If not password correct:
    sleep(amount_of_time)

The question is: what is the value of 'amount_of_time' ?

User: can be annoying to wait 'amount_of_time' for each error
Attacker: keep trying, with lower test / seconds

Third Solution

If not password correct:
    sleep(amount_of_time)
    amount_of_time = amount_of_time * 2

User: less annoying for few password mistakes
Attacker: block the user to connect by sending lot of incorrect password

Fourth Solution

If not password correct for a certain number of time:
    submit a CAPTCHA

User: need to resolve the CAPTCHA (not too complex)
Attacker: need to resolve the CAPTCHA (must be complex)

Good solution (and used by a lot of sites) but be careful to our CAPTCHA. implementation. Anyway there is a trick (see next solution).

Fifth Solution

If not password correct for a certain number of time:
    block the IP
    (eventually) send a reset link

User: User may be blocked because he cannot correctly remember his password.
Attacker: trying the same password with different user, because blocking is based on number of login by user.

Final Solution ?

If several login attempts failed whatever is the user by an IP :
    print a CAPTCHA for this IP

User: User cannot be IP blocked but must remember its password.
Attacker: difficult to have an efficient brute-force attack.

The important notes

Is the login form or the login submit link which is blocked ? Blocking the login form is useless.

Resistance to brute-force is FIRST a problem of password complexity, so you need a strict password policy (especially in the case of distributed brute force).

I don't mention the fact to hash your passwords with salt, you're already doing this right ? Because if it is easier to access to the password database than brute-forcing, the attacker will choose this solution ("A chain is only as strong as its weakest link").

Ritesh Khandekar
  • 3,885
  • 3
  • 15
  • 30
Kartoch
  • 7,610
  • 9
  • 40
  • 68
  • why hasn't this been upvoted yet? Probably the best answer here.. thx. By mentioning sleep I hope you suggest just blocking access to login? Cause using sleep in php would just slow the system if there are many requests. – Sam Vloeberghs Apr 11 '13 at 16:59
  • 1
    About sleep, i was refering to halt script execution before replying to the user request. So I don't think sleep can increase the system charge... – Kartoch Apr 11 '13 at 23:57
  • 1
    More precisely, sleep() is local to the user request treatement, so there is no impact on the other requests... – Kartoch Apr 12 '13 at 08:43
  • but all the users requests are handled by the server ..?! – Sam Vloeberghs Apr 12 '13 at 12:14
  • My point of view was generic (I'm more confident in Java/Servlet/JSP than in PHP), so the sleep() is excepting an interruption of the user request and NOT blocking the whole PHP interpretor. I know it is possible in Java (each user request is dedicated to a thread) but PHP is not multithread and the sleep help page on php.net is not very clear about the real behavior, so I'm not really sure how to handle a sleep to block only one user request. – Kartoch Apr 12 '13 at 13:45
  • From my logs, the attacker may have different IP addresses and use them to continuous brute-force attack. One has been blocked then use another one and another one... I think block by IP is no longer work for today. In some case, an attacker may use simultaneous login attempts (try to login at the same time) from 2 or more zombies. – vee Jun 24 '19 at 13:52
13

I would suggest if the user has tried unsuccessfully, say more than five times and five minutes, you start returning a 503 Service Unavailable immediately, for that IP address. When a login fails, you could use memcache to get the current bad attempts for an IP, and then increment the amount, and save it back to memcache with a 5 minute expiry.

You don't want to put a sleep in your PHP code, as that will allow a single user to create lots of connections to your web server, and potentially bring down other users.

Since the user hasn't logged in, you don't have a session cookie, and if the user is trying to brute force their way into an account, they may not present a cookie at all.

brianegge
  • 29,240
  • 13
  • 74
  • 99
  • sleep "...will allow a single user to create lots of connections". Do you have any sources for that? It's not that I don't believe you, I just would like to read some more about it. – nickf Nov 13 '09 at 06:18
  • 3
    @nickf: well, you don't really need to read a research paper to understand the predicament of using sleep() in this situation. Using sleep() basically pauses the execution of the script, meaning the connection is still open until it continues running. There are limits on how many concurrent connections your server can allow so with sleep() you technically make it easier for hackers to DoS your server ... – Lukman Nov 13 '09 at 06:36
  • why not reroute the ipnr through .htaccess? – Grumpy Jan 20 '12 at 23:16
4

I have used something like this...

  1. Check username and password

    1.1 If no match then, record last failed login time for that combo and number of failed logins.

    1.2 Each fail makes the wait between being able to login something like failsCount * 30 seconds, up to a maximum (such as 10 minutes).

  • This means a brute force attack will exponentially take longer and longer.
  • It could lock a user out - but it will not count a failed login whilst trying to login during the lockout period. This should minimise it.

I've developed this but not released it into the wild yet, so any feedback would be appreciated.

alex
  • 479,566
  • 201
  • 878
  • 984
  • do the failed logins expire? that is, if 2 months ago I forgot my password and tried 6 different things, and now I typed the wrong password, would I be forced to wait 3 minutes before trying again? – nickf Nov 17 '09 at 06:23
  • Umm I think I've done something like `if (timeNow - timeFailed) > 3600` then reset count. – alex Nov 18 '09 at 23:32
  • Nice DoS : an attacker can keep trying false passwords for an user, to forbid him to access. – Kartoch Apr 04 '13 at 11:15
  • 1
    @Kartoch Yes, I believe that's better than eventually allowing the attacker access to their account. – alex Apr 04 '13 at 11:19
2

I am not sure what the best practice is, but when dealing with DoS attacks, a better strategy is to actually divert traffic away from your server. Setting timeouts won't actually help because you're still processing the request and running PHP.

Have you considered setting up another web server running a simpler stripped down version of your login page? When the user tries too many times (e.g., thousands of times), send a message to configure your router and redirect this user to the second web server.

It's like when websites get hit with the slashdot effect, many of them just redirect traffic away until traffic is reduced.

reformed
  • 4,505
  • 11
  • 62
  • 88
Andrew Keith
  • 7,515
  • 1
  • 25
  • 41
  • IMHO, It depends of the motivation of the attacker: if the main goal is light DOS by one user yes it is a possible counter-measure. But if the goal is to avoid brute force of the password, it depends of the behavior of the second web server (dummy ? blocking ?). – Kartoch Apr 12 '13 at 12:59
2

I made a class that takes care of brute force attack protection in PHP.

https://github.com/ejfrancis/BruteForceBlocker

it logs all failed logins site-wide in a db table, and if the number of failed logins in the last 10 minutes (or whatever time frame you choose) is over a set limit, it enforces a time delay and/or a captcha requirement before logging in again.

example:

//build throttle settings array. (# recent failed logins => response).

$throttle_settings = [

    50 => 2,            //delay in seconds
    150 => 4,           //delay in seconds
    300 => 'captcha'    //captcha 

];

$BFBresponse = BruteForceBlocker::getLoginStatus($throttle_settings);

//$throttle_settings is an optional parameter. if it's not included,the default settings array in BruteForceBlocker.php will be used

switch ($BFBresponse['status']){

case 'safe':
    //safe to login
    break;
case 'error':
    //error occured. get message
    $error_message = $BFBresponse['message'];
    break;
case 'delay':
    //time delay required before next login
    $remaining_delay_in_seconds = $BFBresponse['message'];
    break;
case 'captcha':
    //captcha required
    break;

}

ejfrancis
  • 2,925
  • 4
  • 26
  • 42