Block IP address after X amount of failed login attempts

The recommended method of customizing your AppGini-generated application is through hooks. But sometimes you might need to add functionality not accessible through hooks. You can discuss this here.
Post Reply
peebee
AppGini Super Hero
AppGini Super Hero
Posts: 352
Joined: 2013-03-21 04:37

Block IP address after X amount of failed login attempts

Post by peebee » 2022-08-31 09:54

For compliance reasons, I am required to block IP addresses after X amount of failed login attempts in X amount of time.

For that purpose, I have tried the following class code below (sourced from a web search some time ago). It creates a new basic table where IP, Username and a TimeStamp are entered.

The User IP is blocked correctly after X attempts and the block is automatically released (entries for that IP address deleted from the database) on reload of the login page after the set amount of time has expired.

It does work as it is - but it does also have significant limitations/issues and I'm hoping somebody might be able to help refine the process.

Problem 1: an "attempt" entry is made to the database for the IP address EVERY time the login page is LOADED. No need for form submit required to register an attempt. That creates a potential problem where there are multiple Users on the same IP address.

Problem 2: I'd like to use the 'username' variable from the login form input but it appears that $_POST variables are no longer available from the login page (since resources/lib/Authentication.php was introduced in V5.97?)? Tried print_r($_POST) on the login page results Array(). var_dump for the page also results in an empty string? Tried $_Request and (Request::val('username')) but still no luck?

Here's the code added to the top of the login page

Code: Select all

<?php
	$currDir = dirname(__FILE__);
	require_once("${currDir}/hooks/BruteForceClass.php");
	$bf = new framework_BruteForce();	
	$conn = mysqli_connect("localhost", "my-username", "my-password", "my-database");
	$bf->brute_force($conn,300,5,1200);
?>
and here's /hooks/BruteForceClass.php

Code: Select all

<?php
class framework_BruteForce {
	// $conn - sql connection
	// $interval - the duration we are checking (currently 5 minutes or 300 seconds)
	// $count - how many interactions are we allowing in the interval (currently 5)
	// $timeRelease - how long Username is blocked (currently 20 minutes or 1200 seconds)	

	public function brute_force($conn, $interval, $count, $timeRelease) {

		// does the table exist?  If not, create it
		if (!mysqli_query($conn, 'select 1 from `brute_force`')) {
			$sql = "CREATE TABLE `brute_force` (
					`ip_address` VARCHAR(20) NOT NULL,
					`username` VARCHAR(20) NOT NULL,
					`timestamp` datetime)";

			mysqli_query($conn,$sql);
		}
		
		
		// get username
		$username = HOW TO GET USERNAME FROM LOGIN FORM HERE;
		// insert a record for the current interaction				
		$sql = sprintf("INSERT INTO `brute_force` (ip_address,username,timestamp) VALUES('%s','$username',CURRENT_TIMESTAMP)",
			mysqli_real_escape_string($conn,$_SERVER['REMOTE_ADDR']));
			

		mysqli_query($conn,$sql);

		// clean up any items older than our defined interval
		mysqli_query($conn,"DELETE FROM `brute_force` where TIMESTAMPDIFF(SECOND,timestamp,CURRENT_TIMESTAMP) > $timeRelease");

		// check if we have a number of items remaining	
		$sql = sprintf("SELECT COUNT(*) as COUNT from `brute_force` where ip_address = '%s'",
			mysqli_real_escape_string($conn,$_SERVER['REMOTE_ADDR']));

		$result = mysqli_query($conn,$sql);
		$row = mysqli_fetch_assoc($result);
		$ipAdd = $_SERVER['REMOTE_ADDR'];
		
		// if we do, show the block message	
		if($row['COUNT'] > $count) {
			
			
			echo "Your IP: $ipAdd has been temporarily blocked.<br>Too many failed login attempts detected.<br>Please try again in 20 minutes or<br>contact your Database System Admin if you require a password reset";
			exit(0);
		}
	}
}
?>
Ideally, I'd like to:
1. Check for Form submission first (isset) to stop the code from executing immediately on page load
2. Check that a 'username' has been entered to the login form <input> and POST that 'username' to the /hooks/BruteForceClass.php

Even more ideally - as I use reCaptcha to limit actual brute force attacks, check if the submitted 'username' is in the membership_users table and if not - the block could be missed.

Any help or advice most appreciated.

peebee
AppGini Super Hero
AppGini Super Hero
Posts: 352
Joined: 2013-03-21 04:37

Re: Block IP address after X amount of failed login attempts

Post by peebee » 2022-09-06 04:13

I revisted this and think I now have a working solution (or as good as I can get at least). No longer necessary to register Usernames in the new database table. User IP addresses will be blocked for X amount of time after X failed login attempts. Blocks are cleared automatically either after X amount of time or after successful login from the same IP (prior to being blocked of course).

The original code above registered a failed login "attempt" every time the login page was loaded (prior to any attempted sign in), which was far from ideal and a significant problem for multiple Users on the same IP address. That problem is fixed with an attempt being registered only after an unsuccessful "sign in" attempt.

The original code also left the "attempts" in the database until the set $timeRelease had expired. Not an issue for single User IP addresses but that too was a significant problem for multiple users on the one IP. (eg: 4 x failed "attempts' may be registered in the database from a previous user. New User makes 1 attempt and is blocked because limit of 5 x attempts has been reached). An SQL delete in the hooks/__global.php login_ok function fixes that. When a legitimate successful login is made from an already registered IP, all existing attempts for that IP address are deleted.

This code, if anybody chooses to use it, will create a simple new two field table that will regularly clear itself based on user activity.

Insert into the head of your login.php page (If you're using Ronnies excellent AppginiLTE plugin the login page is appginilte_login.php).

Code: Select all

<?php	
	$currDir = dirname(__FILE__);
	require_once("${currDir}/hooks/BlockUserClass.php");
	$bf = new framework_BlockUser();	
	$conn = mysqli_connect("localhost", "your-username", "your-password", "your-database");
	$bf->block_user($conn,300,5,1200);
?>
Edit the

Code: Select all

$bf->block_user($conn,300,5,1200);
times and attempts in the above code to suit your purposes

Create a new hooks/BlockUserClass.php file

Code: Select all

<?php
class framework_BlockUser {
	// $conn - sql connection
	// $interval - the duration we are checking (currently 5 minutes or 300 seconds)
	// $count - how many interactions are we allowing in the interval (currently 5)
	// $timeRelease - how long Username is blocked (currently 20 minutes or 1200 seconds)

	public function block_user($conn, $interval, $count, $timeRelease) {

		// does the table exist?  If not, create it
		if (!mysqli_query($conn, 'select 1 from `block_user`')) {
			$sql = "CREATE TABLE `block_user` (
					`ip_address` VARCHAR(20) NOT NULL,
					`timestamp` datetime)";

			mysqli_query($conn,$sql);
		}
		
		// insert a record for the current interaction		
		if (Request::val('signIn')) return true; 
		$sql = sprintf("INSERT INTO `block_user` (ip_address,timestamp) VALUES('%s',CURRENT_TIMESTAMP)",
			mysqli_real_escape_string($conn,$_SERVER['REMOTE_ADDR']));

		mysqli_query($conn,$sql);

		// clean up any items older than our defined interval
		mysqli_query($conn,"DELETE FROM `block_user` where TIMESTAMPDIFF(SECOND,timestamp,CURRENT_TIMESTAMP) > $timeRelease");

		// check if we have a number of items remaining	
		$sql = sprintf("SELECT COUNT(*) as COUNT from `block_user` where ip_address = '%s'",
			mysqli_real_escape_string($conn,$_SERVER['REMOTE_ADDR']));

		$result = mysqli_query($conn,$sql);
		$row = mysqli_fetch_assoc($result);
		$ipAdd = $_SERVER['REMOTE_ADDR'];
		
		// if we do, show the block message	
		if($row['COUNT'] > $count) {
			
			echo "Your IP: $ipAdd has been blocked.<br>Too many failed login attempts detected.<br>Please contact your Database System Admin if you require a password reset";
			exit(0);
		}
	}
}
?>
Add this line to the hooks/_global.php login_ok function at the top of the file

Code: Select all

	function login_ok($memberInfo, &$args) {
		
	// Delete any blocked IP address record of current user
		sql("DELETE FROM `block_user` where ip_address = '" . $_SERVER["REMOTE_ADDR"] . "'", $abc);	
I am of course open to any improvement anybody has to offer?

Post Reply