ToutSurTout.biz
Sécurisation d'applications Web - Système de bannissement en php


J'ai ajouté mon système de bannissement d'adresse IP au système de login de Codiad. Trop d'essais avec le mauvais mot de passe, et l'adresse IP est bannie pendant 30 minutes. C'est suffisant pour bloquer les attaques de type brute-force. C'est très facile à ajouter, ça m'a pris 2 minutes, et ce genre de manipulation peut se faire sur pratiquement n'importe quelle appli web sans trop de difficultés. Il suffit de localiser l'endroit du code qui vérifie la validité du mot de passe.

Voilà comment procéder pour Codiad: J'ai mis mon petit "ban.php" dans le répertoire /component/user/ de Codiad:

<?php

date_default_timezone_set('Europe/Paris');
$GLOBALS['config']['DATADIR'] = 'data'; // Data subdirectory
$GLOBALS['config']['IPBANS_FILENAME'] = $GLOBALS['config']['DATADIR'].'/ipbans.php'; // File storage for failures and bans.
$GLOBALS['config']['BAN_AFTER'] = 5; // Ban IP after this many failures.
$GLOBALS['config']['BAN_DURATION'] = 1800; // Ban duration for IP address after login failures (in seconds) (1800 sec. = 30 minutes)
if (!is_dir($GLOBALS['config']['DATADIR'])) { mkdir($GLOBALS['config']['DATADIR'],0705); chmod($GLOBALS['config']['DATADIR'],0705); }
if (!is_file($GLOBALS['config']['DATADIR'].'/.htaccess')) { file_put_contents($GLOBALS['config']['DATADIR'].'/.htaccess',"Allow from none\nDeny from all\n"); } // Protect data files.

function logm($message)
{
    $t = strval(date('Y/m/d_H:i:s')).' - '.$_SERVER["REMOTE_ADDR"].' - '.strval($message)."\n";
    file_put_contents($GLOBALS['config']['DATADIR'].'/log.txt',$t,FILE_APPEND);
}


// ------------------------------------------------------------------------------------------
// Brute force protection system
// Several consecutive failed logins will ban the IP address for 30 minutes.
if (!is_file($GLOBALS['config']['IPBANS_FILENAME'])) file_put_contents($GLOBALS['config']['IPBANS_FILENAME'], "<?php\n\$GLOBALS['IPBANS']=".var_export(array('FAILURES'=>array(),'BANS'=>array()),true).";\n?>");
include $GLOBALS['config']['IPBANS_FILENAME'];
// Signal a failed login. Will ban the IP if too many failures:
function ban_loginFailed()
{
    $ip=$_SERVER["REMOTE_ADDR"]; $gb=$GLOBALS['IPBANS'];
    if (!isset($gb['FAILURES'][$ip])) $gb['FAILURES'][$ip]=0;
    $gb['FAILURES'][$ip]++;
    if ($gb['FAILURES'][$ip]>($GLOBALS['config']['BAN_AFTER']-1))
    {
        $gb['BANS'][$ip]=time()+$GLOBALS['config']['BAN_DURATION'];
        logm('IP address banned from login');
    }
    $GLOBALS['IPBANS'] = $gb;
    file_put_contents($GLOBALS['config']['IPBANS_FILENAME'], "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>");
}

// Signals a successful login. Resets failed login counter.
function ban_loginOk()
{
    $ip=$_SERVER["REMOTE_ADDR"]; $gb=$GLOBALS['IPBANS'];
    unset($gb['FAILURES'][$ip]); unset($gb['BANS'][$ip]);
    $GLOBALS['IPBANS'] = $gb;
    file_put_contents($GLOBALS['config']['IPBANS_FILENAME'], "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>");
    logm('Login ok.');
}

// Checks if the user CAN login. If 'true', the user can try to login.
function ban_canLogin()
{
    $ip=$_SERVER["REMOTE_ADDR"]; $gb=$GLOBALS['IPBANS'];
    if (isset($gb['BANS'][$ip]))
    {
        // User is banned. Check if the ban has expired:
        if ($gb['BANS'][$ip]<=time())
        { // Ban expired, user can try to login again.
            logm('Ban lifted.');
            unset($gb['FAILURES'][$ip]); unset($gb['BANS'][$ip]);
            file_put_contents($GLOBALS['config']['IPBANS_FILENAME'], "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>");
            return true; // Ban has expired, user can login.
        }
        return false; // User is banned.
    }
    return true; // User is not banned.
}

?>

puis j'ai modifié class.user.php. Voici le diff:

--- class.user.php.original    2013-08-07 18:28:37.000000000 +0200
+++ class.user.php    2013-08-07 18:31:38.000000000 +0200
@@ -5,6 +5,7 @@
*  as-is and without warranty under the MIT License. See
*  [root]/license.txt for more. This information must remain intact.
*/
+require_once 'ban.php';

class User {

@@ -54,9 +55,10 @@
                 if($user['project']!=''){ $_SESSION['project'] = $user['project']; }
             }
         }
-
-        if($pass){ echo formatJSEND("success",array("username"=>$this->username)); }
-        else{ echo formatJSEND("error","Incorrect Username or Password"); }
+       
+        if (!ban_canLogin()) { $pass=false; }
+        if($pass){ ban_loginOk(); echo formatJSEND("success",array("username"=>$this->username)); }
+        else{ ban_loginFailed(); echo formatJSEND("error","Incorrect Username or Password"); }
     }

Vous voyez, il n'y a vraiment pas grand chose à changer: 2 lignes à ajouter, et 2 lignes à modifier, et voilà un formulaire de login protégé contre le brute-force.  :-)

La petite subtilité ici, sur la manière dont je l'ai intégré, c'est que le pirate ne pourra pas faire la différence entre un bannissement et un mot de passe incorrecte: S'il est banni, on continuera à lui répondre que le mot de passe est incorrecte, même s'il est bon. Gniark gniark.