Simple PHP anti-brute-force login

You may have a simple site protected by a simple PHP username/password login script. The site may be so simple that the actual username/password and all other configuration settings are stored in plain-text files on the backend server and you don't even make use of a database.

How do you protect your site from someone that's trying to break your login though? Someone that writes a script and tries thousands of different username/password pairs per second? Well for one it helps if the username is something other than just 'admin'. But also, you want to prevent scripted login attempts by restricting the number of invalid attempts per hour. That is, after 3 incorrect login attempts, lock out the user account for 10 minutes. This means a hacker can try at most 18 password combinations per hour.

But if you lock by username, the hacker can use this to discover valid users, then try to crack the password. You can alternatively lock out after 3 invalid login attempts from a particular IP address. Hackers could only bypass this by either using a lot of proxies, or performing a distributed zombie attack, but that takes considerably more effort and determination. Your simple site is probably not worth the effort for that.

The following is a simple PHP static class with a utility function to help you do this:


<?php

class Services {


//call with no parameters to get a true/false response. If true, do not process login.
//call with parameter set to true to register a new failed attempt for current user and return a true/false response.
public static function bruteCheck($failed_attempt = false) {
$deny_login = false;

if(!file_exists(MM_BRUTE_FILE)) touch(MM_BRUTE_FILE);
$cache = unserialize(Services::fileToString(MM_BRUTE_FILE));
if(!$cache) $cache = array();

if($failed_attempt) { //register the new failed attempt and timestamp
if(!isset($cache[$_SERVER['REMOTE_ADDR']])) {
$cache[$_SERVER['REMOTE_ADDR']] = array();
}
$cache[$_SERVER['REMOTE_ADDR']][] = time();
if(count($cache[$_SERVER['REMOTE_ADDR']]) > MM_BRUTE_ATTEMPTS) array_shift($cache[$_SERVER['REMOTE_ADDR']]);
}

//get the number of failed attempts in the last 15 minutes
if(!isset($cache[$_SERVER['REMOTE_ADDR']])) {
$deny_login = false;
} else {
$attempts = $cache[$_SERVER['REMOTE_ADDR']];
if(count($attempts) < MM_BRUTE_ATTEMPTS) {
$deny_login = false;
} else {
if($attempts[0] + MM_BRUTE_WINDOW > time()) $deny_login = true;
else $deny_login = false;
}
}

//cleanup the cache so it doesn't get too large over time
foreach($cache as $ip=>$attempts) {
if($attempts) {
if($attempts[count($attempts)-1] + MM_BRUTE_WINDOW < time()) {
unset($cache[$ip]);
}
}
}

Services::stringToFile(MM_BRUTE_FILE, serialize($cache));

return $deny_login;
}

public static function fileToString($filename) {
return file_get_contents($filename);
}

public static function stringToFile($filename, $data) {
$file = fopen ($filename, "w");
fwrite($file, $data);
fclose ($file);
return true;
}
}
?>


To use it, just modify your login procedure as follows:

$deny_login = Services::bruteCheck();
if($deny_login) {
$login_err = "Login locked. Try again in 15 minutes.";
} else {
//check login credentials
if($incorrect_login) {
Services::bruteCheck(true);
$login_err = "Invalid username or password!";
} else {
//user is logged in
}
}

The bruteCheck function maintains an associative array that collects the number of failed login attempts per IP address. This is cached to the file-system as a serialized PHP object. Edit the constant params as needed to point to the location of this cache file, the number of allowed login attempts, and the lockout window. For example (I usually stick these in my main config.php which includes all application settings):

define('MM_BRUTE_FILE', 'c:/temp/brute.txt');
define('MM_BRUTE_WINDOW', 15*60);
define('MM_BRUTE_ATTEMPTS', 5);

Comments

  1. Thanks for this simple ('simple' being GOOD; over-complex security measures are often worse than none!) brute force protection class - where should I declare MM_BRUTE_WINDOW & MM_BRUTE_ATTEMPTS and with what type of content (would be nice with few examples).

    ReplyDelete
  2. I've updated the post to show an example of the config definitions. Just stick these in a config.php file, or anywhere before calling the bruteCheck function.

    ReplyDelete
  3. Very nice tutorial. Thank you for sharing.
    Need to know how to put: define('MM_BRUTE_FILE', '');
    Is it path on visitor's local computers or at the server host?

    ReplyDelete
  4. Edited...
    There is a file created on my root folder.
    The text showed up on the file, it said ' a:0:{} '
    But..
    After several incorrect password attempts, the text remained the same, i.e a:0:{}, and the script doesn't block the correct one.

    ReplyDelete
    Replies
    1. You need to insert Services::bruteCheck(true); after failed login attempt in your script. Had the same error, now it works just fine.

      Delete
  5. Hello

    I've found your script against BF. I've found it clear and simple as I wanted but I can't succeed to install it. I tried half of the day using Google to help but I give up now (2 a.m).

    I have tested on the simple way, could u help me a little please :

    File A.php :

    <form action="B.php" method="post">

    <input type="password" name="mot_de_passe" />
    <input type="submit" value="Valider" />

    </form>

    File B.php

    <php
    include 'config.php'; // contains 3 defines function
    include 'BF.php'; // contains your PHP class

    if (isset($_POST['mot_de_passe']) AND $_POST['mot_de_passe'] == "RIGHTPSWD")
    {echo ' It works !'}
    else
    {echo 'Byebye'}
    ?

    >

    I tried to modifiy the login procedure, but I failed. Could U help me a little ?
    Thanks and Regards

    ReplyDelete
  6. please provide demo & demo download files

    ReplyDelete

Post a Comment

Popular posts from this blog

Wkhtmltopdf font and sizing issues

Import Google Contacts to Nokia PC Suite

Can't delete last blank page from Word