HOWTOlabs Web Programming
PHP and javascript together to detext malicious login attempts

Login Flood Control
rickatech 2019-11

Related [edit]

Inevitably a web page or application evolves to the point where it benefits from users logging in to see content and services tailored to their needs.  Enabling login with a username and password checked against a simple list or sophisticated backend database intrinsically is rather straight forward.

However, anything that can be accessed on the Internet can also be attacked en masse by essentially anonymous malicious web crawling agents, often referred to as 'bots' or 'botnets'.  The agents exploit crimes of opportunity, and will check the dynamics of web login pages for simple ways to gain access.  A time honored tactic is to randomly attempt to guess login and passwords, and if a site is not sophisticated, thousands of guesses per second can be made.  This page illustrates a flood control approach to detect excessive login attempts in a short period of time from the same IP address.  There are more sophisticated mechanisms for thwarting malicious agents trying to crack a web login, but this approach is quite effective in significantly increasing the amount of work needed to hack a login.  The more time and work required to hack a site, the less chance that agents will bother, moving on to less difficult targets.

<?php
/*  https://www.php.net/manual/en/features.http-auth.php  */
/*  stand-alone http auth digest unit test  */
/*  https://stackoverflow.com/questions/3079031/login-form-for-http-basic-auth  */

//  Attempting to flood control bots that rapid fire / brute force attempts to
//  guess the username / password.  If the same IP attempts to login more than
//  thrice in 30 seconds, consider it a bot flood ... abort request.
//  Once session starts and flood activates, subsequently login retries
//  limied to 1 per 30 second period.
session_start();
if (isset(
$_GET['logout'])) {
    
//  typically this request invoked from background AJAX call
    
auth_logout();
    die (
'[ please press back button, reload ]');
}

if (isset(
$_SESSION['allowed']))
    
$adb 'allowed set';
else {
    
$adb 'allowed not set';
    if (isset(
$_SESSION['ip'])) {
      
/*  seconds to delay if flood detected  */
      
$delay = isset($_SESSION['fdelay']) ? $_SESSION['fdelay'] : 30;
      if (
$_SESSION['last_post'] + $delay time()) {
        
//  FUTURE - what if different clents from same IP are attacking, keep flood block IP on server for a few minutes?
        
if (isset($_SESSION['flood']))
            
$_SESSION['flood']++;
        else
            
$_SESSION['flood'] = 1;
        if (
$_SESSION['flood'] > 6)
            die(
'too early, I hope you are not a malicious bot'.auth_adb());
        }
        else {
            
//  after delay period, allow auth attempts again
            
$_SESSION['flood'] = 1;
            
$_SESSION['fdelay'] = $delay 2;
        }
      }
    
$_SESSION['last_post'] = time();
    
$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
    
// store the message
    
}

  if (isset(
$_GET['login'])) {  /*  GET login  */
    
$realm 'Restricted area';

    
//user => password
    
$users = array('admin' => 'mypass''guest' => 'guest');

    if (empty(
$_SERVER['PHP_AUTH_DIGEST'])) {
        
$allowed_not 'Text to send if user hits Cancel button';
    } else if (!(
$data http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) ||
        
// analyze the PHP_AUTH_DIGEST variable
        
!isset($users[$data['username']])) {
        
$allowed_not 'Wrong Credentials! (parse)';
    } else {
        
// generate the valid response
        
$A1 md5($data['username'] . ':' $realm ':' $users[$data['username']]);
        
$A2 md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
        
$valid_response md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);
        if (
$data['response'] != $valid_response)
            
$allowed_not 'Wrong Credentials! (valid response)';
    }
    if (isset(
$allowed_not))
        
auth_header();
    else {
        
auth_login($data['username']);
        if (isset(
$_GET['login']))
            die (
'[ please press back button, reload ]');
    }
  }  
/*  GET login  [ end ]  */

//  ok, valid username & password

if (isset($_SESSION['allowed'])) {
    
//  sending logout as form request avoids problematic GET variable 
    //  however, it created a warning on reload that may have some confusing side effects?
    //  better, actuall use a javascript ajax with GET request,
    //  have that 'endpoint' clear the allowed session variable, when it returns force page reload
    
echo 'You are logged in as: ' $_SESSION['username'];
    echo 
"\n<p><a href=?logout>logout</a></p>";

} else {  
//  logged out
    
if (isset($allowed_not))
    echo 
$allowed_not;
    else 
    echo 
'You are logged out';  ?>

<form onsubmit="return login_check()">
username <input type=text name=username id=username>
<br>password <input type=password name=password id=password><?PHP
    
echo "\n<input type=hidden value=".$_SERVER['PHP_SELF'].' id=pathfile>';
    echo 
"\n<input type=hidden value=".$_SERVER['HTTP_HOST'].' id=domain>';
    echo 
"\n<input type=hidden value=".($_SERVER['SERVER_PORT'] == 80 'http://' 'https://').' id=port>';  ?>
<br><input type=submit>
<span id=wait>[ enter login ]</span>
<a style="display: none;" id=log_link href=#>login</a>
</form>

<script language=javascript>
function login_check() {
    u = document.getElementById("username").value;
    if (u + '#' == '#') { alert('username required'); return false; }
    p = document.getElementById("password").value;
    if (p + '#' == '#') { alert('password required'); return false; }
    f = document.getElementById("pathfile").value;
    t = document.getElementById("domain").value;
    s = document.getElementById("port").value;
    document.getElementById("wait").style.display = 'none';
    document.getElementById("log_link").href = s + u + ':' + p + '@' + t + f + '?login';
    document.getElementById("log_link").style.display = 'inline';
    /*   window.location = s + u + ':' + p + '@' + t + f + '?login';  */
    return false;
}
</script>  <?PHP
}

echo 
auth_adb();


// function to parse the http auth header
function http_digest_parse($txt)
{
    
// protect against missing data
    
$needed_parts = array('nonce'=>1'nc'=>1'cnonce'=>1'qop'=>1'username'=>1'uri'=>1'response'=>1);
    
$data = array();
    
$keys implode('|'array_keys($needed_parts));

    
preg_match_all('@(' $keys ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@'$txt$matchesPREG_SET_ORDER);

    foreach (
$matches as $m) {
        
$data[$m[1]] = $m[3] ? $m[3] : $m[4];
        unset(
$needed_parts[$m[1]]);
    }

    return 
$needed_parts false $data;
}


function 
auth_header() {
    
//  this must be called also to logout out
    
$realm 'Restricted area';
    
header('HTTP/1.1 401 Unauthorized');
    
header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');
}

function 
auth_logout() {
    if (isset(
$_SESSION['allowed'])) {
        unset(
$_SESSION['allowed']);
        unset(
$_SESSION['username']);
        unset(
$_SESSION['last_post']);
        unset(
$_SESSION['ip']);
        
auth_header();
    }
}

function 
auth_login($un) {
    
//  since you are valid, update session variable to keep login logic from triggering unnecesaily
    
$_SESSION['username'] = $un;
    
$_SESSION['allowed'] = time();
    unset(
$_SESSION['ip']);
    unset(
$_SESSION['flood']);
    unset(
$_SESSION['fdelay']);
}

function 
auth_adb() {
    global 
$t$adb;

    return
        
"\n\n<pre>".$adb.
        
"\nusername:  ".$_SESSION['username'].
        
"\nlast_post: ".$_SESSION['last_post'].
        
"\ntime:      ".$t.
        
"\nflood:     ".$_SESSION['flood'].
        
"\nfdelay:    ".$_SESSION['fdelay'].
        
"\nip:        ".$_SESSION['ip'].'</pre>';
}
?>