web-dev-qa-db-fra.com

Comment puis-je limiter les tentatives de connexion de l'utilisateur dans PHP

Je venais de lire cet article Le guide ultime pour l'authentification de sites Web basée sur des formulaires sur la prévention des tentatives de connexion Rapid-Fire. 

Meilleure pratique n ° 1: Un court délai qui augmente avec le nombre de tentatives infructueuses, comme:

1 tentative infructueuse = pas de retard
2 tentatives infructueuses = délai de 2 secondes
3 tentatives infructueuses = délai de 4 secondes
4 tentatives infructueuses = délai de 8 secondes
5 tentatives infructueuses = délai de 16 secondes
etc. 

DoS attaquer ce schéma serait très peu pratique, mais en revanche potentiellement dévastateur, car le délai augmente de manière exponentielle.

Je suis curieux de savoir comment je pourrais implémenter quelque chose comme ceci pour mon système de connexion en PHP?

53
JasonDavis

Vous ne pouvez pas simplement empêcher les attaques DoS en chaînant la limitation à une seule adresse IP ou nom d'utilisateur. Enfer, vous ne pouvez même pas vraiment empêcher les tentatives de connexion rapide avec cette méthode.  

Pourquoi?Parce que l'attaque peut s'étendre sur plusieurs adresses IP et comptes d'utilisateurs dans le but de contourner vos tentatives de limitation.

J'ai vu posté ailleurs qu'idéalement, vous devriez suivre toutes les tentatives de connexion infructueuses sur le site et les associer à un horodatage, par exemple:

CREATE TABLE failed_logins (
    id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(16) NOT NULL,
    ip_address INT(11) UNSIGNED NOT NULL,
    attempted DATETIME NOT NULL,
    INDEX `attempted_idx` (`attempted`)
) engine=InnoDB charset=UTF8;

Une petite remarque sur le champ ip_address: Vous pouvez stocker les données et les récupérer, respectivement, avec INET_ATON () et INET_NTOA (), ce qui revient essentiellement à convertir une adresse ip en un entier non signé.

# example of insertion
INSERT INTO failed_logins SET username = 'example', ip_address = INET_ATON('192.168.0.1'), attempted = CURRENT_TIMESTAMP;
# example of selection
SELECT id, username, INET_NTOA(ip_address) AS ip_address, attempted;

Décidez de certains seuils de délai en fonction du nombre global de connexions échouées au cours d'une période donnée (15 minutes dans cet exemple). Vous devez vous baser sur les données statistiques extraites de votre table failed_logins car elles changeront avec le temps en fonction du nombre d'utilisateurs et du nombre d'utilisateurs pouvant rappeler (et taper) leur mot de passe.


> 10 failed attempts = 1 second
> 20 failed attempts = 2 seconds
> 30 failed attempts = reCaptcha

Interrogez la table à chaque tentative de connexion échouée pour trouver le nombre de connexions échouées pour une période donnée, par exemple 15 minutes:


SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute);

Si le nombre de tentatives sur la période donnée dépasse votre limite, appliquez la limitation ou forcez tous les utilisateurs à utiliser un captcha (c'est-à-dire reCaptcha) jusqu'à ce que le nombre de tentatives ayant échoué au cours de la période donnée soit inférieur au seuil. 

// array of throttling
$throttle = array(10 => 1, 20 => 2, 30 => 'recaptcha');

// retrieve the latest failed login attempts
$sql = 'SELECT MAX(attempted) AS attempted FROM failed_logins';
$result = mysql_query($sql);
if (mysql_affected_rows($result) > 0) {
    $row = mysql_fetch_assoc($result);

    $latest_attempt = (int) date('U', strtotime($row['attempted']));

    // get the number of failed attempts
    $sql = 'SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute)';
    $result = mysql_query($sql);
    if (mysql_affected_rows($result) > 0) {
        // get the returned row
        $row = mysql_fetch_assoc($result);
        $failed_attempts = (int) $row['failed'];

        // assume the number of failed attempts was stored in $failed_attempts
        krsort($throttle);
        foreach ($throttle as $attempts => $delay) {
            if ($failed_attempts > $attempts) {
                // we need to throttle based on delay
                if (is_numeric($delay)) {
                    $remaining_delay = time() - $latest_attempt - $delay;
                    // output remaining delay
                    echo 'You must wait ' . $remaining_delay . ' seconds before your next login attempt';
                } else {
                    // code to display recaptcha on login form goes here
                }
                break;
            }
        }        
    }
}

L'utilisation de reCaptcha à un certain seuil permettrait de s'assurer qu'une attaque provenant de plusieurs fronts serait arrêtée et que les utilisateurs normaux du site ne subiraient aucun retard important pour les tentatives de connexion légitimes ayant échoué.

74
Corey Ballou

Vous avez trois approches de base: stocker les informations de session, stocker les informations de cookie ou stocker les informations IP.

Si vous utilisez des informations de session, l’utilisateur final (attaquant) peut invoquer de force de nouvelles sessions, contourner votre tactique, puis vous reconnecter sans délai. Les sessions sont assez simples à mettre en œuvre, il suffit de stocker la dernière heure de connexion connue de l'utilisateur dans une variable de session, de la faire correspondre à l'heure actuelle et de s'assurer que le délai a été suffisamment long.

Si vous utilisez des cookies, l'attaquant peut tout simplement les rejeter. En tout et pour tout, ce n'est vraiment pas une solution viable.

Si vous suivez les adresses IP, vous devrez enregistrer les tentatives de connexion à partir d'une adresse IP, de préférence dans une base de données. Lorsqu'un utilisateur tente de se connecter, il vous suffit de mettre à jour votre liste d'adresses IP enregistrée. Vous devez purger cette table à un intervalle raisonnable, en déchargeant les adresses IP inactives depuis un certain temps. Le piège (il y a toujours un piège), c'est que certains utilisateurs peuvent finir par partager une adresse IP et que, dans certaines conditions, vos retards peuvent affecter les utilisateurs par inadvertance. Puisque vous suivez les connexions échouées, et seulement les connexions échouées, cela ne devrait pas causer trop de douleur.

5
Mark Elliot

Le processus de connexion doit réduire sa vitesse de connexion pour une connexion réussie ou non. La tentative de connexion elle-même ne devrait jamais être supérieure à environ 1 seconde. Si c'est le cas, la force brute utilise le délai pour savoir que la tentative a échoué car le succès est plus court que l'échec. Ensuite, plus de combinaisons peuvent être évaluées par seconde. 

Le nombre de tentatives de connexion simultanées par ordinateur doit être limité par l'équilibreur de charge. Enfin, il vous suffit de suivre si le même utilisateur ou le même mot de passe est réutilisé par plusieurs tentatives de connexion utilisateur/mot de passe. Les humains ne peuvent pas taper plus vite qu’environ 200 mots par minute. Ainsi, les tentatives de connexion successives ou simultanées supérieures à 200 mots par minute proviennent d'un ensemble de machines. Ceux-ci peuvent donc être ajoutés à une liste noire en toute sécurité, car ce n'est pas votre client. Les temps de liste noire par hôte ne doivent pas nécessairement être supérieurs à environ 1 seconde. Cela ne gênera jamais un humain, mais fera des ravages avec une tentative de force brute, que ce soit en série ou en parallèle.

Il faudra 158 ans pour que 2 * 10 ^ 19 combinaisons à une combinaison par seconde, exécutées en parallèle sur 4 milliards d'adresses IP distinctes, s'épuisent comme espace de recherche. Pour durer un jour par utilisateur contre 4 milliards d'attaques, vous avez besoin d'un mot de passe alphanumérique entièrement aléatoire d'une longueur minimale de 9 places. Envisagez de former les utilisateurs avec des phrases de passe d'au moins 13 caractères, 1.7 * 10 ^ 20 combinaisons.

Ce délai incitera l'attaquant à voler votre fichier de hachage de mot de passe plutôt que de forcer brutalement votre site. Utilisez des techniques de hachage approuvées, nommées. Interdire l’ensemble de la population d’IP Internet pendant une seconde limitera l’effet d’attaques parallèles sans la moindre appréciation d’un humain. Enfin, si votre système autorise plus de 1 000 tentatives d'ouverture de session échouées en une seconde sans intervention pour interdire les systèmes, vos plans de sécurité ont de gros problèmes à résoudre. Corrigez d'abord cette réponse automatisée.

4
Don Turnblade
session_start();
$_SESSION['hit'] += 1; // Only Increase on Failed Attempts
$delays = array(1=>0, 2=>2, 3=>4, 4=>8, 5=>16); // Array of # of Attempts => Secs

sleep($delays[$_SESSION['hit']]); // Sleep for that Duration.

ou comme suggéré par Cyro:

sleep(2 ^ (intval($_SESSION['hit']) - 1));

C'est un peu dur, mais les composants de base sont là. Si vous actualisez cette page, chaque délai sera prolongé.

Vous pouvez également conserver les comptes dans une base de données, où vous vérifiez le nombre de tentatives infructueuses par IP. En l'utilisant en mode IP et en gardant les données de votre côté, vous empêchez l'utilisateur de supprimer les cookies pour arrêter le retard.

Fondamentalement, le code de début serait:

$count = get_attempts(); // Get the Number of Attempts

sleep(2 ^ (intval($count) - 1));

function get_attempts()
{
    $result = mysql_query("SELECT FROM TABLE WHERE IP=\"".$_SERVER['REMOTE_ADDR']."\"");
    if(mysql_num_rows($result) > 0)
    {
        $array = mysql_fetch_assoc($array);
        return $array['Hits'];
    }
    else
    {
        return 0;
    }
}
3
Tyler Carter

Enregistrer les tentatives infructueuses dans la base de données par IP. (Puisque vous avez un système de connexion, je suppose que vous savez bien le faire.)

De toute évidence, les sessions sont une méthode tentante, mais une personne vraiment dédiée peut facilement se rendre compte qu’elle peut simplement supprimer son cookie de session en cas de tentative infructueuse afin de contourner complètement le papillon.

Lors de la tentative de connexion, récupérez le nombre de tentatives de connexion récentes (par exemple, les 15 dernières minutes) et l'heure de la dernière tentative.

$failed_attempts = 3; // for example
$latest_attempt = 1263874972; // again, for example
$delay_in_seconds = pow(2, $failed_attempts); // that's 2 to the $failed_attempts power
$remaining_delay = time() - $latest_attempt - $delay_in_seconds;
if($remaining_delay > 0) {
    echo "Wait $remaining_delay more seconds, silly!";
}
3
Matchu

IMHO, la défense contre les attaques DOS est mieux traitée au niveau du serveur Web (ou même du matériel réseau), pas dans votre code PHP.

2
vicatcu

Vous pouvez utiliser des sessions. À chaque fois que l'utilisateur échoue lors d'une connexion, vous augmentez la valeur stockant le nombre de tentatives. Vous pouvez calculer le délai requis à partir du nombre de tentatives ou définir le temps réel pendant lequel l'utilisateur est autorisé à réessayer dans la session.

Une méthode plus fiable consisterait à stocker les tentatives et le nouveau délai d’essai dans la base de données pour cette adresse IP particulière.

2
Sampson

Les cookies ou les méthodes basées sur les sessions sont bien sûr inutiles dans ce cas. L'application doit vérifier l'adresse IP ou l'horodatage (ou les deux) des tentatives de connexion précédentes.

Une vérification IP peut être contournée si l'attaquant dispose de plus d'une adresse IP pour lancer ses demandes et peut être gênante si plusieurs utilisateurs se connectent à votre serveur à partir de la même adresse IP. Dans ce dernier cas, une personne qui échouerait à se connecter plusieurs fois empêcherait tous ceux qui partagent la même adresse IP de se connecter avec ce nom d'utilisateur pendant un certain temps.

Un problème d'horodatage a le même problème que précédemment: tout le monde peut empêcher tout le monde de se connecter à un compte particulier en essayant plusieurs fois. Utiliser un captcha au lieu d’une longue attente pour la dernière tentative est probablement une bonne solution de contournement.

Les seules choses supplémentaires que le système de connexion devrait éviter sont les conditions de concurrence dans la fonction de vérification des tentatives. Par exemple, dans le pseudocode suivant

$time = get_latest_attempt_timestamp($username);
$attempts = get_latest_attempt_number($username);

if (is_valid_request($time, $attempts)) {
    do_login($username, $password);
} else {
    increment_attempt_number($username);
    display_error($attempts);
}

Que se passe-t-il si un attaquant envoie simultanément demandes à la page de connexion? Toutes les demandes s'exécuteraient probablement avec la même priorité et il est probable qu'aucune demande ne parvienne à l'instruction increment_attempt_number avant que les autres n'aient dépassé la deuxième ligne. Ainsi, chaque demande obtient la même valeur $ time et $ tent et est exécutée. Empêcher ce type de problèmes de sécurité peut être difficile pour des applications complexes et implique le verrouillage et le déverrouillage de certaines tables/lignes de la base de données, ce qui ralentit évidemment l'application.

1
user225840

La réponse courte est: ne faites pas ceci. Vous ne vous protégerez pas du forçage brutal, vous pourriez même aggraver votre situation.

Aucune des solutions proposées ne fonctionnerait. Si vous utilisez l'adresse IP en tant que paramètre de limitation, l'attaquant ne fera qu'étaler l'attaque sur un très grand nombre d'adresses IP. Si vous utilisez la session (cookie), l'attaquant ne fera que déposer les cookies. La somme de tout ce que vous pouvez penser, c'est qu'il n'y a absolument rien qu'un attaquant forcé brutal ne puisse surmonter.

Cependant, il y a une chose - vous vous fiez simplement au nom d'utilisateur qui a essayé de se connecter. Donc, ne regardez pas tous les autres paramètres que vous suivez, la fréquence à laquelle un utilisateur a essayé de se connecter et de contrôler. Mais un attaquant veut vous faire du mal. S'il le reconnaît, il utilisera également des noms d'utilisateur en force brute.

Ainsi, presque tous vos utilisateurs seront limités à leur valeur maximale lorsqu'ils essaieront de se connecter. Votre site Web sera inutile. Attaquant: succès.

Vous pouvez généralement retarder la vérification du mot de passe d’environ 200 ms - l’utilisateur du site Web ne le remarquera presque pas. Mais un brute-forcer le fera. (Encore une fois, il pourrait couvrir plusieurs adresses IP) Cependant, rien de tout cela ne vous protégera contre le brute forcing ou les attaques DDoS, car vous ne pouvez pas programmer.

La seule façon de faire est d'utiliser l'infrastructure.

Vous devez utiliser bcrypt au lieu de MD5 ou SHA-x pour hacher vos mots de passe, cela rendra le déchiffrement de vos mots de passe beaucoup plus difficile si quelqu'un vole votre base de données (car je suppose que vous êtes sur un hôte partagé ou géré).

Désolé de vous avoir déçu, mais toutes les solutions présentées ici ont une faiblesse et il n’ya aucun moyen de les surmonter dans la logique du back-end.

1
nico gawenda

Comme indiqué dans la discussion ci-dessus, les sessions, les cookies et les adresses IP ne sont pas efficaces - ils peuvent tous être manipulés par l'attaquant.

Si vous souhaitez empêcher les attaques par force brute, la seule solution pratique consiste à baser le nombre de tentatives sur le nom d'utilisateur fourni. Notez toutefois que cela permet à l'attaquant de doser le site sous DOS en empêchant les utilisateurs valides de se connecter.

par exemple.

$valid=check_auth($_POST['USERNAME'],$_POST['PASSWD']);
$delay=get_delay($_POST['USERNAME'],$valid);

if (!$valid) {
   header("Location: login.php");
   exit;
}
...
function get_delay($username,$authenticated)
{
    $loginfile=SOME_BASE_DIR . md5($username);
    if (@filemtime($loginfile)<time()-8600) {
       // last login was never or over a day ago
       return 0;
    }
    $attempts=(integer)file_get_contents($loginfile);
    $delay=$attempts ? pow(2,$attempts) : 0;
    $next_value=$authenticated ? 0 : $attempts + 1;
    file_put_contents($loginfile, $next_value);
    sleep($delay); // NB this is done regardless if passwd valid
    // you might want to put in your own garbage collection here
 }

Notez que cette procédure, telle qu’elle est écrite, fuit des informations de sécurité - c’est-à-dire qu’une personne attaquant le système pourra voir quand un utilisateur se connecte (le temps de réponse de la tentative d’attaquant passera à 0). Vous pouvez également régler l'algorithme de sorte que le délai soit calculé en fonction du délai précédent et de l'horodatage du fichier.

HTH

C.

1
symcbean

Je crée généralement un historique de connexion et des tables de tentatives de connexion. La table de tentatives consignera le nom d'utilisateur, le mot de passe, l'adresse IP, etc. Effectuez une requête sur la table pour voir si vous devez attendre. Je recommanderais de bloquer complètement les tentatives supérieures à 20 dans un temps donné (une heure par exemple).

1
sestocker

cballuo a fourni une excellente réponse. Je voulais juste retourner la faveur en fournissant une version mise à jour qui prend en charge mysqli. J'ai légèrement modifié les colonnes table/field dans les fichiers sqls et autres petites choses, mais cela devrait aider ceux qui cherchent l'équivalent de mysqli.

function get_multiple_rows($result) {
  $rows = array();
  while($row = $result->fetch_assoc()) {
    $rows[] = $row;
  }
  return $rows;
}

$throttle = array(10 => 1, 20 => 2, 30 => 5);

$query = "SELECT MAX(time) AS attempted FROM failed_logins";    

if ($result = $mysqli->query($query)) {

    $rows = get_multiple_rows($result);

$result->free();

$latest_attempt = (int) date('U', strtotime($rows[0]['attempted'])); 

$query = "SELECT COUNT(1) AS failed FROM failed_logins WHERE time > DATE_SUB(NOW(), 
INTERVAL 15 minute)";   

if ($result = $mysqli->query($query)) {

$rows = get_multiple_rows($result);

$result->free();

    $failed_attempts = (int) $rows[0]['failed'];

    krsort($throttle);
    foreach ($throttle as $attempts => $delay) {
        if ($failed_attempts > $attempts) {
                echo $failed_attempts;
                $remaining_delay = (time() - $latest_attempt) - $delay;

                if ($remaining_delay < 0) {
                echo 'You must wait ' . abs($remaining_delay) . ' seconds before your next login attempt';
                }                

            break;
        }
     }        
  }
}
0
jason328