Here today at Codingsec we look at how to protect your login areas from Brute force attacks. A Brute force attack also known as a dictionary attack is a technique where security experts or hackers, try words in the dictionary, common used phrases and numerical combinations. Securing your log in area is extremely significant.
The method that we will show you today is called Queuing, this keeps login attempts to a minimum preventing a Brute force attack.
Methods of limiting log in attempts
There are alternative methods, specifically created to prevent brute-forcing, but vast amounts of them have issues that make them unsuitable.Below are a list of other techniques that are commonly used to prevent Brute force attacks, and their downfalls:
- A commonly used technique is to monitor the number of login attempts made during a session – or just a cookie – if there an excess amount of unsuccessful login attempts, the client is usually blocked from attempting to log in for a period of time. The problem with this method is that it relies on the user-agent to maintain a session/cookie. Those can be blocked or deleted very easily, which would bypass the block entirely.
- A variation on the above technique is to log the IP address rather than using a session or cookie. This technique deciphers the implausibility of the cookies, although it initiates other issues. IP addresses are not distinctly unique.When blocking an IP, you could in fact be blocking large volumes of unrelated users.IP addresses are easily customised or masked, hackers can also initiate other IP addresses from hacked networks. To outline this monitoring/blocking of IP addresses is not a fully effective technique.]
- In order to surpass the above issues, simply remove the reliance on client identification and instead block the user who registers to many log in attempts. The significant issue here is that any prankster could easily keep large amounts of users blocked indefinitely by routinely sending a number of invalid log in attempts.
- Yet another attempt to defy brute-forcing is to slow down the requests themselves, making brute force attacks too slow to be of use. This – in theory – is an good plan, and is in fact the basis of the queuing method I will be demonstrating. However, many implement this rather poorly. We’ve seen people simply drop a sleep(1); into all login code, making the request take 1 second before it completes. The issue with this approach is that even though you are slowing down the request, you aren’t really preventing the hacker from making an obscene amount of attempts. It’ll just take one second longer for the results to start piling in. Issuing 50 requests per second won’t cause those 50 requests to take 50 seconds, it’ll only cause them to take one second + the time it takes each request to execute.
Issues with queuing
So is queueing free of all those problems? No, definitely not. The main problem you may face with a queueing system is DOS attacks. Similar to that of the third method I describe above, a prankster could easily keep the queue full of invalid requests and make normal login requests take intolerably long. An attacker may also try to continually execute several requests per second, which would in no time overload the server with queued login attempts, potentially even crashing it. (Depending on the server config.)
However, there are ways to minimize these risks:
- By adding a total queue size you could stop the server from becoming overloaded with login attempts. It would simply drop login requests once the queue has reached a certain size.
- By splitting the queue into per-user queues, at least a prankster would not be able to stall all attempts by keeping the queue full, only those meant for specific users. (Unless the number of targeted users allows them to exceeds the overall queue size.)
- By only allowing one queue entry per IP address, you would prevent simple attacks from a single source. An attacker would have to use several IP addresses to make any difference. (Not that it would stop anybody determined, but it may be enough to get script kiddies just messing with your queue times to lose interest.) – It’s worth noting that, as always, any restrictions based on IP addresses are not exactly reliable, and can cause issues for some users. Organizations, for example, frequently fall under the same network routers or proxies, and all the users within that network will therefore share an external IP. Only one of those users could use the system at a time if an IP restriction like this is put in place. – Consider it very carefully before deciding to add such a restriction.
In the example I will be implementing here, I will demonstrate all three of these measures. Don’t take that to mean you should necessarily do so as well!
The Theory
The key to deterring brute-force attacks is to delay each request long enough for the attack to become impractical. If you can only test one password per second, then it’ll take forever to test them all. This makes Brute force attacks a waste of time as they will take too long, normal users will not be affected by this delay.
So, how do we implement this in PHP? The effective technique is to set up a database where each login attempt is entered and an ID is generated for it. The attempt with the lowest ID will then be allowed to be processed, after which it is removed from the database, and the attempt following it is allowed to proceed. The best practice is to have a background process that handles the validation; finding the first unprocessed attempt, validating it, updating it’s status in the database, and moving on to the next one. Requests for login attempts would add their entries to the database, and then periodically check it to see if their attempt has been processed yet, after which they remove the attempt from the database and return the result to the user.
However, background processes can be troublesome for vast amounts of PHP hosts. The technique demonstrated is where each request is responsible for validating their own attempts. They add an entry to the database, then periodically check the database to see if their entry is the first entry listed, then process it, and finally remove it. Simple enough.
Implementation
The first thing we need to do here is set up a database. Because of it’s general availability in the world of PHP, I’m going to use MySQL as my database. Note, however, that you may just as well use in-memory systems like Memcache or the APC extension’s user cache mechanisms. Those would in fact most likely perform better.
The table I’m going to use will contain four columns:
- An ID column that we can use to order the attempts, and find out which attempt is next to be processed.
- A last_checked column, which will be updated by each attempt each time the code checks if the attempt is ready. This last_checked column will be used to filter out dead attempts; attempts added by requests that have since been killed off. If we don’t take this precaution, any dead request will stall the entire queue until it’s manually removed.
- An ip_address column, which will store the unsigned integer representation of the client’s IP address. This column will have a UNIQUE key restraint on it, to make sure that each IP can only exist once in the queue. (You could just as easily store the IP string, but I have a thing about wasted storage space.)
- A user name column, to store the name of the user that attempt is waiting for. This will be used to split the queue up into per-user queues. This means that even if there are ten attempts queued up for one user, an attempt for another user will not have to wait.
1 | CREATE TABLE `login_attempt_queue` ( |
2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, |
3 | `last_checked` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, |
4 | `ip_address` INT UNSIGNED NOT NULL, |
5 | `username` VARCHAR(100) NOT NULL, |
7 | UNIQUE KEY(`ip_address`) |
Additionally, since this is a user login system, I will be using this as the table where the password hashes we are validating are stored:
2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, |
3 | `username` VARCHAR(100) NOT NULL, |
4 | `password` CHAR(60) NOT NULL, |
To manage the entire process, the creation of a class can be used to create login attempts, which waits for them to be processed, and then handle the validation result.
Note that this code uses the PHP 5.5 password hashing functions. If you do not have PHP 5.5, there are 3rd party libraries that let you easily add this functionality to older versions. (Down to 5.3, with that particular library.)
020 | const ATTEMPT_DELAY = 1000; |
027 | const ATTEMPT_EXPIRATION_TIMEOUT = 5000; |
032 | const MAX_PER_USER = 5; |
037 | const MAX_OVERALL = 30; |
063 | private $isLoginValid ; |
079 | private $readyCheckStatement ; |
087 | private $checkUpdateStatement ; |
097 | public function __construct( $username , $password , \PDO $pdo ) |
100 | if ( $this ->pdo->getAttribute(PDO::ATTR_ERRMODE) !== PDO::ERRMODE_EXCEPTION) { |
101 | $this ->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); |
104 | $this ->username = $username ; |
105 | $this ->password = $password ; |
107 | if (! $this ->isQueueSizeExceeded()) { |
111 | throw new Exception( "Queue size has been exceeded." , 503); |
120 | private function addToQueue() |
122 | $sql = "INSERT INTO login_attempt_queue (ip_address, username) |
124 | $stmt = $this ->pdo->prepare( $sql ); |
126 | $stmt ->execute( array ( |
127 | sprintf( '%u' , ip2long ( $_SERVER [ "REMOTE_ADDR" ])), |
130 | $this ->attemptID = (int) $this ->pdo->lastInsertId(); |
132 | catch (PDOException $e ) { |
133 | throw new Exception( "IP address is already in queue." , 403); |
143 | private function isQueueSizeExceeded() |
147 | COUNT (IF(username = ?, TRUE, NULL)) AS user |
148 | FROM login_attempt_queue |
149 | WHERE last_checked > NOW() - INTERVAL ? MICROSECOND"; |
150 | $stmt = $this ->pdo->prepare( $sql ); |
151 | $stmt ->execute( array ( |
153 | self::ATTEMPT_EXPIRATION_TIMEOUT * 1000 |
156 | $count = $stmt ->fetch(PDO::FETCH_OBJ); |
158 | throw new Exception( "Failed to query queue size" , 500); |
161 | return ( $count ->overall >= self::MAX_OVERALL || $count ->user >= self::MAX_PER_USER); |
170 | private function isReady() |
172 | if (! $this ->readyCheckStatement) { |
173 | $sql = "SELECT id FROM login_attempt_queue |
175 | last_checked > NOW() - INTERVAL ? MICROSECOND AND |
179 | $this ->readyCheckStatement = $this ->pdo->prepare( $sql ); |
181 | $this ->readyCheckStatement->execute( array ( |
182 | self::ATTEMPT_EXPIRATION_TIMEOUT * 1000, |
185 | $result = (int) $this ->readyCheckStatement->fetchColumn(); |
187 | if (! $this ->checkUpdateStatement) { |
188 | $sql = "UPDATE login_attempt_queue |
189 | SET last_checked = CURRENT_TIMESTAMP |
190 | WHERE id = ? LIMIT 1"; |
191 | $this ->checkUpdateStatement = $this ->pdo->prepare( $sql ); |
193 | $this ->checkUpdateStatement->execute( array ( $this ->attemptID)); |
195 | return $result === $this ->attemptID; |
205 | public function isValid() |
207 | if ( $this ->isLoginValid === null) { |
208 | $sql = "SELECT password |
211 | $stmt = $this ->pdo->prepare( $sql ); |
212 | $stmt ->execute( array ( $this ->username)); |
213 | $realHash = $stmt ->fetchColumn(); |
216 | $this ->isLoginValid = password_verify( $this ->password, $realHash ); |
219 | $this ->isLoginValid = false; |
223 | usleep(self::ATTEMPT_DELAY * 1000); |
227 | $sql = " DELETE FROM login_attempt_queue |
230 | last_checked < NOW() - INTERVAL ? MICROSECOND"; |
231 | $stmt = $this ->pdo->prepare( $sql ); |
232 | $stmt ->execute( array ( |
234 | self::ATTEMPT_EXPIRATION_TIMEOUT * 1000 |
238 | return $this ->isLoginValid; |
248 | public function whenReady( $callback , $checkTimer =250) |
250 | while (! $this ->isReady()) { |
251 | usleep( $checkTimer * 1000); |
254 | if ( is_callable ( $callback )) { |
255 | call_user_func( $callback , $this ->isValid()); |
Usage
To sum up the functionality of the class, we only have two public methods we need to concern ourselves with.
- __construct creates the attempt, taking the username, the password and a PDO instance. It sets their respective class attributes to those values, and then triggers the addToQueue function, which goes on to create a new database entry for the class and set the attemptID attribute.
- whenReady is our “listener”, so to speak. It takes a callable function as the first parameter, and optionally a delay timer value as the second parameter. It will keep calling the isReady function in a loop, each iteration delayed by the value of that second parameter, until it returns TRUE, thus reporting that the attempt is next in line to be processed. Then it will go on to call the the isValid function, which checks the validity of the attempt and removes it from the database. Finally it calls the callback function, and passes the validity value with it as it’s only parameter.
Here is an example of how this could be used on a login form’s action page:
02 | require "LoginAttempt.php" ; |
04 | if (! empty ( $_POST [ "username" ]) && ! empty ( $_POST [ "password" ])) { |
05 | $dsn = "mysql:host=localhost;dbname=test" ; |
06 | $pdo = new PDO( $dsn , "username" , "password" ); |
09 | $attempt = new LoginAttempt( $_POST [ "username" ], $_POST [ "password" ], $pdo ); |
10 | $attempt ->whenReady( function ( $success ) { |
11 | echo $success ? "Valid" : "Invalid" ; |
14 | catch (Exception $e ) { |
15 | if ( $e ->getCode() == 503) { |
16 | header( "HTTP/1.1 503 Service Unavailable" ); |
19 | else if ( $e ->getCode() == 403) { |
20 | header( "HTTP/1.1 403 Forbidden" ); |
24 | echo "Error: " . $e ->getMessage(); |
36 | echo "Error: Missing user input." ; |
For PHP 5.2 or lower, the above method of using a closure for the whenReady function is not possible. Instead you would have to define a function, and then pass the name of it as the first parameter to whenReady:
1 | function onReady( $isValid ) { |
2 | echo $isValid ? "Valid" : "Invalid" ; |
5 | $attempt = new LoginAttempt( $_POST [ "username" ], $_POST [ "password" ], $pdo ); |
6 | $attempt ->whenReady( "onReady" ); |
Conclusion
If using this technique, be aware that certain things need to be taken in to consideration. Make sure the ATTEMPT_DELAY value is appropriate. The value of 1000 ms presented is just a suggestion. You may want to alter this and tailor it to the amount of web traffic you get on your server. Make sure the execution time of the login script is also appropriately set. Requests may need to wait in line for some time, so make sure your PHP doesn’t cancel the request before it is processed effectively. Don’t set it too high either; request lingering open forever isn’t a good thing. It is essential to tailor the configuration to your specific needs, I hope you have enjoyed reading this article stay tuned for more soon!
Comments
Post a Comment