Combining methods 3 and 4 from the original post into a kind of 'fuzzy' or dynamic whitelist, and then - and here's the trick - not blocking non-whitelisted IPs, just throttling them to hell and back.
Note that this measure is only meant to thwart this very specific type of attack. In practice, of course, it would work in combination with other best-practices approaches to auth: fixed-username throttling, per-IP throttling, code-enforced strong password policy, unthrottled cookie login, hashing all password equivalents before saving them, never using security questions, etc.
Assumptions about the attack scenario
If an attacker is targeting variable usernames, our username throttling doesn't fire. If the attacker is using a botnet or has access to a large IP range, our IP throttling is powerless. If the attacker has pre-scraped our userlist (usually possible on open-registration web services), we can't detect an ongoing attack based on number of 'user not found' errors. And if we enforce a restrictive system-wide (all usernames, all IPs) throttling, any such attack will DoS our entire site for the duration of the attack plus the throttling period.
So we need to do something else.
The first part of the countermeasure: Whitelisting
What we can be fairly sure of, is that the attacker is not able to detect and dynamically spoof the IP addresses of several thousand of our users(+). Which makes whitelisting feasible. In other words: for each user, we store a list of the (hashed) IPs from where the user has previously (recently) logged in.
Thus, our whitelisting scheme will function as a locked 'front door', where a user must be connected from one of his recognized 'good' IPs in order to log in at all. A brute-force attack on this 'front door' would be practically impossible(+).
(+) unless the attacker 'owns' either the server, all our users' boxes, or the connection itself -- and in those cases, we no longer have an 'authentication' issue, we have a genuine franchise-sized pull-the-plug FUBAR situation
The second part of the countermeasure: System-wide throttling of unrecognized IPs
In order to make a whitelist work for an open-registration web service, where users switch computers frequently and/or connect from dynamic IP addresses, we need to keep a 'cat door' open for users connecting from unrecognized IPs. The trick is to design that door so botnets get stuck, and so legitimate users get bothered as little as possible.
In my scheme, this is achieved by setting a very restrictive maximum number of failed login attempts by unapproved IPs over, say, a 3-hour period (it may be wiser to use a shorter or longer period depending on type of service), and making that restriction global, ie. for all user accounts.
Even a slow (1-2 minutes between attempts) brute force would be detected and thwarted quickly and effectively using this method. Of course, a really slow brute force could still remain unnoticed, but too slow speeds defeat the very purpose of the brute force attack.
What I am hoping to accomplish with this throttling mechanism is that if the maximum limit is reached, our 'cat door' slams closed for a while, but our front door remains open to legitimate users connecting by usual means:
The only legitimate users who would be affected during an attack - ie. while the throttling was activated - would be users without persistent login cookies who were logging in from an unknown location or with a dynamic IP. Those users would be unable to login until the throttling wore off (which could potentially take a while, if the attacker kept his botnet running despite the throttling).
To allow this small subset of users to squeeze through the otherwise-sealed cat door, even while bots were still hammering away at it, I would employ a 'backup' login form with a CAPTCHA. So that, when you display the "Sorry, but you can't login from this IP address at the moment" message, include a link that says "secure backup login - HUMANS ONLY (bots: no lying)". Joke aside, when they click that link, give them a reCAPTCHA-authenticated login form that bypasses the site-wide throttling. That way, IF they are human AND know the correct login+password (and are able to read CAPTCHAs), they will never be denied service, even if they are connecting from an unknown host and not using the autologin cookie.
Oh, and just to clarify: Since I do consider CAPTCHAs to be generally evil, the 'backup' login option would only appear while throttling was active.
There is no denying that a sustained attack like that would still constitute a form of DoS attack, but with the described system in place, it would only affect what I suspect to be a tiny subset of users, namely people who don't use the "remember me" cookie AND happen to be logging in while an attack is happening AND aren't logging in from any of their usual IPs AND who can't read CAPTCHAs. Only those who can say no to ALL of those criteria - specifically bots and really unlucky disabled people - will be turned away during a bot attack.
EDIT: Actully, I thought of a way to let even CAPTCHA-challenged users pass through during a 'lockdown': instead of, or as a supplement to, the backup CAPTCHA login, provide the user with an option to have a single-use, user-specific lockdown code sent to his email, that he can then use to bypass the throttling. This definitely crosses over my 'annoyance' threshold, but since it's only used as a last resort for a tiny subset of users, and since it still beats being locked out of your account, it would be acceptable.
(Also, note that none of this happens if the attack is any less sophisticated than the nasty distributed version I've described here. If the attack is coming from just a few IPs or only hitting a few usernames, it will be thwarted much earlier, and with no site-wide consequences)
So, that is the countermeasure I will be implementing in my auth library, once I'm convinced that it's sound and that there isn't a much simpler solution that I've missed. The fact is, there are so many subtle ways to do things wrong in security, and I'm not above making false assumptions or hopelessly flawed logic. So please, any and all feedback, criticism and improvements, subtleties etc. are highly appreciated.
A few simple steps:
Blacklist certain common usernames, and use them as a honeypot. Admin, guest, etc... Don't let anyone create accounts with these names, so if someone does try to log them in you know it's someone doing something they shouldn't.
Make sure anyone who has real power on the site has a secure password. Require admins/ moderators to have longer passwords with a mix of letters, numbers and symbols. Reject trivially simple passwords from regular users with an explanation.
One of the simplest things you can do is tell people when someone tried to log into their account, and give them a link to report the incident if it wasn't them. A simple message when they log in like "Someone tried to log into your account at 4:20AM Wednesday blah blah. Click here if this wasn't you." It lets you keep some statistics on attacks. You can step up monitoring and security measures if you see that there's a sudden increase in fraudulent accesses.
If I understand the MO of brute force attacks properly, then one or more usernames are tried continuously.
There are two suggestions which I don't think I've seen yet here:
Edit: In response to comments on a username throttle: this is a username specific throttle without regard to the source of the attack.
If the username is throttled, then even a coordinated username attack (multi IP, single guess per IP, same username) would be caught. Individual usernames are protected by the throttle, even if the attackers are free to try another user/pass during the timeout.
From an attackers point of view, during the timeout you may be able to take a first time guess at 100 passwords, and quickly discover one wrong password per account. You may only be able to make a 50 second guesses for the same time period.
From a user account point of view, it still takes the same average number of guesses to break the password, even if the guesses are coming from multiple sources.
For the attackers, at best, it will be the same effort to break 100 accounts as it would 1 account, but since you're not throttling on a site wide basis, you can ramp up the throttle quite quickly.
Extra refinements:
UI ideas (may not be suitable in this context), which may also refine the above:
There are three factors of authentication:
Usually, websites only enforce policy #1. Even most banks only enforce policy 1. They instead rely on a "knows something else" approach to two-factor authentication. (IE: A user knows their password and their mother's maiden name.) If you are able, a way to add in a second factor of authentication is not too difficult.
If you can generate around 256 characters of randomness, you could structure that in a 16×16 table, and then ask the user to give you the value in the table of cell A-14, for example. When a user signs up or changes their password, give them the table and tell them to print it off and save it.
The difficulty with that approach is that when a user forgets their password, as they will, you can't just offer the standard "answer this question and put in a new password", since that's vulnerable to brute-force as well. Also, you can't reset it and email them a new one, since their email could be compromised as well. (See: Makeuseof.com and their stolen domain.)
Another idea (which involves kittens), is what BOA calls SiteKey (I believe they trademarked the name). Briefly, you have the user upload an image when they register, and when they attempt to login, ask them to pick their image out of 8 or 15 (or more) random ones. So, if a user uploads a picture of their kitten, theoretically only they know exactly which picture is theirs out of all the other kittens (or flowers or whatever). The only real vunerability this approach has is the man-in-the-middle attack.
One more idea (no kittens though), is to track IPs that users access the system with, and require them to perform additional authentication (captcha , pick a kitty, pick a key from this table) when they log in from an address they haven't before. Also, similar to GMail, allow the user to view where they have logged in from recently.
Edit, New Idea:
Another way of validating login attempts is to check whether or not the user has come from your login page. You can't check referrers, since they can be easily faked. What you need is to set a key in the _SESSION var when the user views the login page, and then check to make sure that key exists when they submit their login information. If bot does not submit from the login page, it will not be able to login. You can also facilitate this by involving javascript in the process, either by using it to set a cookie, or adding some information to the form after it has loaded. Or, you can split the form up into two different submits (ie, the user enters their username, submits, then on a new page enters their password and submit again.)
The key, in this case, is the most important aspect. A common method of generating them is some combination of the user's data, their IP, and the time it was submitted.
I had previously answered a very similar question over at How can I throttle user login attempts in PHP. I'll reiterate the proposed solution here as I believe many of you will find it informational and useful to see some actual code. Please bare in mind that using a CAPTCHA might not be the best solution due to the increasingly accurate algorithms being used in CAPTCHA busters nowadays:
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
) engine=InnoDB charset=UTF8;
Decide on certain delays based on the overall number of failed logins in a given amount of time. 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');
// assume query result of $sql is stored in $row
$sql = 'SELECT MAX(attempted) AS attempted FROM failed_logins';
$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)';
// 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 minimized and normal site users would not experience a significant delay for legitimate failed login attempts. I can't gaurantee prevention, as it's already been expanded upon that CAPTCHA's can be busted. There are alternative solutions, perhaps a variant of "Name this animal", which could work quite well as a substitute.
I have to ask whether you've done cost-benefit analysis of this problem; it sounds like you're trying to protect yourself from an attacker who has enough web presence to guess a number of passwords, sending maybe 3-5 requests per IP (since you've dismissed IP throttling). How much (roughly) would that kind of attack cost? Is it more expensive than the value of the accounts you're trying to protect? How many gargantuan botnets want what you've got?
The answer might be no -- but if it is, I hope you're getting help from a security professional of some sort; programming skill (and StackOverflow score) do not correlate strongly to security know-how.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With