How to protect your website from Brute force attacks using queues.

Follow by Email

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.

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` (
4     `ip_address` INT UNSIGNED NOT NULL,
5     `username` VARCHAR(100) NOT NULL,
6     PRIMARY KEY (`id`),
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:

1 CREATE TABLE `users` (
3     `username` VARCHAR(100) NOT NULL,
4     `password` CHAR(60) NOT NULL,
5     PRIMARY KEY (`id`)
6 ) ENGINE=InnoDB;

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.)

001 <?php
003 /**
004  * Provides everything needed to manage and use a login queue.
005  * The login queue will store each login attempt in a database table
006  * and process the entries one at at time, with a delay between
007  * each one. The idea here is to make brute-force attacks on the
008  * login system impractically slow.
009  *
010    *
011  *
012    *
013  *                      added the single IP restriction.
014  */
015 class LoginAttempt
016 {
017     /**
018      * @var int The number of milliseconds to sleep between login attempts.
019      */
020     const ATTEMPT_DELAY = 1000;
022     /**
023      * @var int The number of milliseconds before an unchecked attempt is
024      *          considered dead.
025      *
026      */
027     const ATTEMPT_EXPIRATION_TIMEOUT = 5000;
029     /**
030      * @var int Number of queued attempts allowed per user.
031      */
032     const MAX_PER_USER = 5;
034     /**
035      * @var int Number of queued attempts allowed overall.
036      */
037     const MAX_OVERALL = 30;
039     /**
040      * The ID assigned to this attempt in the database.
041      *
042      * @var int
043      */
044     private $attemptID;
046     /**
047      * @var string
048      */
049     private $username;
051     /**
052      * @var string
053      */
054     private $password;
056     /**
057      * After the login has been validated, this attribute will hold the
058      * result. Subsequent calls to isValid will return this value, rather
059      * that try to validate it again.
060      *
061      * @var bool
062      */
063     private $isLoginValid;
065     /**
066      * An open PDO instance.
067      *
068      * @var PDO
069      */
070     private $pdo;
072     /**
073      * Stores the statement used to check whether the attempt is ready to be processed.
074      * As it may be used multiple times per attempt, it makes sense not to initialize
075      * it each ready check.
076      *
077      * @var PDOStatement
078      */
079     private $readyCheckStatement;
081     /**
082      * The statement used to update the attempt entry in the database on
083      * each isReady call.
084      *
085      * @var PDOStatement
086      */
087     private $checkUpdateStatement;
089     /**
090      * Creates a login attempt and queues it.
091      *
092      * @param string $username
093      * @param string $password
094      * @var \PDO $pdo
095      * @throws Exception
096      */
097     public function __construct($username, $password, \PDO $pdo)
098     {
099         $this->pdo = $pdo;
100         if ($this->pdo->getAttribute(PDO::ATTR_ERRMODE) !== PDO::ERRMODE_EXCEPTION) {
101             $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
102         }
104         $this->username = $username;
105         $this->password = $password;
107         if (!$this->isQueueSizeExceeded()) {
108             $this->addToQueue();
109         }
110         else {
111             throw new Exception("Queue size has been exceeded.", 503);
112         }
113     }
115     /**
116      * Creates an entry for the attempt in the database, fetching the id
117      * of it and storing it in the class. Note that no values need to
118      * be entered in the database; the defaults for both columns are fine.
119      */
120     private function addToQueue()
121     {
122         $sql = "INSERT INTO login_attempt_queue (ip_address, username)
123                 VALUES (?, ?)";
124         $stmt = $this->pdo->prepare($sql);
125         try {
126             $stmt->execute(array(
127                 sprintf('%u', ip2long($_SERVER["REMOTE_ADDR"])),
128                 $this->username
129             ));
130             $this->attemptID = (int)$this->pdo->lastInsertId();
131         }
132         catch (PDOException $e) {
133             throw new Exception("IP address is already in queue.", 403);
134         }
135     }
137     /**
138      * Checks the queue size. Throws an exception if it has been exceeded. Otherwise it does nothing.
139      *
140      * @throws Exception
141      * @return bool
142      */
143     private function isQueueSizeExceeded()
144     {
145         $sql = "SELECT
146                     COUNT(*) AS overall,
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(
152             $this->username,
153             self::ATTEMPT_EXPIRATION_TIMEOUT * 1000
154         ));
156         $count = $stmt->fetch(PDO::FETCH_OBJ);
157         if (!$count) {
158             throw new Exception("Failed to query queue size", 500);
159         }
161         return ($count->overall >= self::MAX_OVERALL || $count->user >= self::MAX_PER_USER);
162     }
164     /**
165      * Checks if the login attempt is ready to be processed, and updates the
166      * last_checked timestamp to keep the attempt alive.
167      *
168      * @return bool
169      */
170     private function isReady()
171     {
172         if (!$this->readyCheckStatement) {
173             $sql = "SELECT id FROM login_attempt_queue
174                     WHERE
175                         last_checked > NOW() - INTERVAL ? MICROSECOND AND
176                         username = ?
177                     ORDER BY id ASC
178                     LIMIT 1";
179             $this->readyCheckStatement = $this->pdo->prepare($sql);
180         }
181         $this->readyCheckStatement->execute(array(
182             self::ATTEMPT_EXPIRATION_TIMEOUT * 1000,
183             $this->username
184         ));
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);
192         }
193         $this->checkUpdateStatement->execute(array($this->attemptID));
195         return $result === $this->attemptID;
196     }
198     /**
199      * Checks if the login attempt is valid. Note that this function will cause
200      * the delay between attempts when first called. If called multiple times,
201      * only the first call will do so.
202      *
203      * @return bool
204      */
205     public function isValid()
206     {
207         if ($this->isLoginValid === null) {
208             $sql = "SELECT password
209                     FROM users
210                     WHERE username = ?";
211             $stmt = $this->pdo->prepare($sql);
212             $stmt->execute(array($this->username));
213             $realHash = $stmt->fetchColumn();
215             if ($realHash) {
216                 $this->isLoginValid = password_verify($this->password, $realHash);
217             }
218             else {
219                 $this->isLoginValid = false;
220             }
222             // Sleep at this point, to enforce a delay between login attempts.
223             usleep(self::ATTEMPT_DELAY  * 1000);
225             // Remove the login attempt from the queue, as well as any login
226             // attempt that has timed out.
227             $sql = "DELETE FROM login_attempt_queue
228                     WHERE
229                         id = ? OR
230                         last_checked < NOW() - INTERVAL ? MICROSECOND";
231             $stmt = $this->pdo->prepare($sql);
232             $stmt->execute(array(
233                 $this->attemptID,
234                 self::ATTEMPT_EXPIRATION_TIMEOUT * 1000
235             ));
236         }
238         return $this->isLoginValid;
239     }
241     /**
242      * Calls the callback function when the login attempt is ready, passing along the
243      * result of the validation as the first parameter.
244      *
245      * @param callable|string $callback
246      * @param int $checkTimer Delay between checks, in milliseconds.
247      */
248     public function whenReady($callback, $checkTimer=250)
249     {
250         while (!$this->isReady()) {
251             usleep($checkTimer * 1000);
252         }
254         if (is_callable($callback)) {
255             call_user_func($callback, $this->isValid());
256         }
257     }
258 }

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:

01 <?php
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");
08     try {
09         $attempt = new LoginAttempt($_POST["username"], $_POST["password"], $pdo);
10         $attempt->whenReady(function($success) {
11             echo $success ? "Valid" : "Invalid";
12         });
13     }
14     catch (Exception $e) {
15         if ($e->getCode() == 503) {
16             header("HTTP/1.1 503 Service Unavailable");
17             exit;
18         }
19         else if ($e->getCode() == 403) {
20             header("HTTP/1.1 403 Forbidden");
21             exit;
22         }
23         else {
24             echo "Error: " . $e->getMessage();
25         }
27         // Note here that it may be advisable to show the
28         // same response for error messages that you show
29         // for invalid requests. That way it'll be less
30         // obvious to attackers that their requests are
31         // being rejected rather than processed and
32         // invalidated.
33     }
34 }
35 else {
36     echo "Error: Missing user input.";
37 }

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";
3 }
5 $attempt = new LoginAttempt($_POST["username"], $_POST["password"], $pdo);
6 $attempt->whenReady("onReady");

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!

Follow by Email


  1. Paul 2015-01-13 Reply
  2. vic511 2015-08-28 Reply

Add a Comment

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

Like the article? please consider sharing it. Thank you

Advertisment ad adsense adlogger