Comment puis-je étouffer les tentatives de connexion des utilisateurs en PHP
j'étais en train de lire ce post le guide définitif pour l'authentification de site web basée sur les formulaires sur la prévention des tentatives de connexion rapide-feu.
pratique exemplaire no 1: un court délai qui augmente avec le nombre de tentatives infructueuses, comme:
1 tentative ratée = aucun retard
2 tentatives infructueuses = 2 sec délai
3 tentatives infructueuses = 4 sec délai
Quatre tentatives infructueuses = délai de 8 sec
5 tentatives infructueuses = retard de 16 secondes
etc.
DoS attaquer ce schéma serait très peu pratique, mais d'un autre côté, potentiellement dévastateur, puisque le délai augmente exponentiellement.
je suis curieux de savoir comment j'ai pu implémenter quelque chose comme ça pour mon système de login en PHP?
12 réponses
vous ne pouvez pas simplement empêcher les attaques DoS en enchaînant throttling vers le bas à une seule IP ou un nom d'utilisateur. Vous ne pouvez même pas empêcher les tentatives de connexion rapide en utilisant cette méthode.
pourquoi? parce que l'attaque peut s'étendre à plusieurs IPs et comptes d'utilisateurs pour éviter vos tentatives d'étranglement.
j'ai vu posté ailleurs que Idéalement vous devriez être suivi toutes les tentatives de connexion échoué à travers le site et les associer à un horodatage, peut-être:
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 () qui équivalent essentiellement à convertir une adresse ip en et à partir d'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écider de certains seuils de retard sur la base de la globale nombre de connexions manquées dans une période donnée (15 minutes dans cet exemple). Vous devez baser cela sur des données statistiques tirées de votre table failed_logins
comme il sera changement dans le temps basé sur le nombre d'utilisateurs et combien d'entre eux peuvent se rappeler (et dactylographier) leur mot de passe.
> 10 failed attempts = 1 second
> 20 failed attempts = 2 seconds
> 30 failed attempts = reCaptcha
interrogez la table sur chaque tentative de connexion manquée pour trouver le nombre de connexions manquées pour une période de temps donnée, disons 15 minutes:
SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute);
si le nombre de tentatives au cours de la période de temps donnée est supérieur à votre limite, soit forcer l'étranglement ou forcer tous les utilisateurs à utiliser un captcha (c.-à-d. reCaptcha) jusqu'à ce que le nombre de tentatives ratées au cours de la période de temps 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 garantirait qu'un les attaques provenant de plusieurs fronts seraient stoppées et les utilisateurs normaux du site ne connaîtraient pas de retard important pour les tentatives légitimes de connexion.
vous avez trois approches de base: stocker des informations de session, stocker des informations de cookie ou stocker des informations IP.
si vous utilisez des informations de session, l'utilisateur final (attaquant) pourrait invoquer de force de nouvelles sessions, contourner votre tactique, puis se connecter à nouveau 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 l'apparier à l'heure actuelle, et de s'assurer que le délai a été assez long.
si vous utilisez des cookies, l'attaquant peut simplement rejeter les cookies, dans l'ensemble, ce n'est vraiment pas quelque chose de viable.
si vous suivez les adresses IP, vous aurez besoin de stocker les tentatives de connexion à partir d'une adresse IP d'une manière ou d'une autre, de préférence dans une base de données. Quand un utilisateur essaie de se connecter, mettez simplement à jour votre liste enregistrée de IPs. Vous devez purger cette table à un intervalle raisonnable, en renonçant aux adresses IP qui n'ont pas été actives depuis un certain temps. Le piège (il y a toujours un piège), est que certains utilisateurs peuvent finir par partager une adresse IP, et dans des conditions limites vos retards peuvent affecter les utilisateurs par inadvertance. Puisque vous traquez les logins défectueux, et seulement les logins défectueux, cela ne devrait pas causer trop de douleur.
le processus de connexion doit réduire sa vitesse de connexion à la fois réussie et infructueuse. La tentative de connexion elle-même ne devrait jamais être plus rapide qu'environ 1 seconde. Si c'est le cas, brute force utilise le délai pour savoir que la tentative a échoué parce que 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 machine doit être limité par l'équilibreur de charge. Enfin, vous avez juste besoin de suivre si l' même utilisateur ou mot de passe est utilisé par plus d'un utilisateur/mot de passe tentative de connexion. Les humains ne peuvent pas taper plus vite qu'environ 200 mots par minite. Ainsi, les tentatives de connexion successives ou simultanées plus de 200 mots par minute proviennent d'un ensemble de machines. Ceux-ci peuvent ainsi être acheminé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 être supérieurs à environ 1 seconde. Cela ne gênera jamais un humain, mais fait des ravages avec une tentative de force brute que ce soit en série ou en parallèle.
2 * 10^19 les combinaisons à une combinaison par seconde, fonctionnant en parallèle sur 4 milliards d'adresses IP séparées, prendront 158 ans à être épuisées en tant qu'espace de recherche. Pour durer un jour par utilisateur contre 4 milliards d'attaquants, vous avez besoin d'un mot de passe alphanumérique entièrement aléatoire 9 places long à un minimum. Envisager la formation des utilisateurs en phrases de passe d'au moins 13 places, en combinaisons 1,7 * 10^20.
ce délai, motivera l'attaquant de voler votre mot de passe fichier de hachage plutôt que de forcer votre site. Utiliser des techniques de hachage approuvées, nommées. Interdire la totalité de la population de la propriété intellectuelle sur Internet pendant une seconde, limitera l'effet des attaques parallèles sans un deal qu'un humain apprécierait. Enfin, si votre système permet plus de 1000 tentatives ratées de connexion en une seconde sans réponse aux systèmes d'interdiction, alors vos plans de sécurité ont de plus gros problèmes à résoudre. Répare d'abord cette réponse automatisée.
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 rude, mais les composants de base sont là. Si vous rafraîchissez cette page, chaque fois que vous rafraîchissez le délai sera plus long.
vous pouvez également garder les comptes dans une base de données, où vous vérifiez le nombre de tentatives ratées par IP. En l'utilisant, basé sur l'IP et de conserver les données de votre côté, vous empêcher l'utilisateur d'être en mesure d'effacer ses cookies pour arrêter le retard.
en gros, 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;
}
}
enregistre les tentatives d'échec dans la base de données par IP. (Puisque vous avez un système de connexion, je suppose que vous savez très bien comment faire.)
évidemment, les sessions est une méthode tentante, mais quelqu'un vraiment consacré peut très facilement se rendre compte qu'ils peuvent simplement supprimer leur cookie de session sur les tentatives ratées afin de contourner la manette des gaz entièrement.
sur la tentative de se connecter, aller chercher combien de récentes (disons, 15 dernières minutes) tentatives de connexion il y avait, et le 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!";
}
vous pouvez utiliser des sessions. Chaque fois que l'utilisateur échoue un login, vous augmentez la valeur stockant le nombre de tentatives. Vous pouvez déterminer le délai requis à partir du nombre de tentatives, ou vous pouvez définir la durée réelle de l'utilisateur est autorisé à essayer de nouveau dans la session.
une méthode plus fiable serait de stocker les tentatives et le nouveau-essai-temps dans la base de données pour cette adresse IP particulière.
IMHO, la défense contre les attaques DOS est mieux traitée au niveau du serveur web (ou peut-être même dans le matériel réseau), pas dans votre code PHP.
je crée généralement l'historique de connexion et les tables de tentative de connexion. La table attempt enregistrerait le nom d'utilisateur, le mot de passe, l'adresse ip, etc. Requête sur la table pour voir si vous avez besoin d'attendre. Je recommande le blocage complet pour les tentatives supérieures à 20 dans un temps donné (une heure par exemple).
comme indiqué ci - dessus, les sessions, les cookies et les adresses IP ne sont pas efficaces-tous peuvent être manipulés par l'attaquant.
si vous voulez empêcher les attaques de force brute, alors la seule solution pratique est de baser le nombre de tentatives sur le nom d'utilisateur fourni, cependant notez que cela permet à l'attaquant de DOS le site en empêchant les utilisateurs valides de se connecter.
p.ex.
$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 tel qu'écrit, ce la procédure fuit les informations de sécurité-c'est-à-dire qu'il sera possible pour quelqu'un qui attaque le système de voir quand un utilisateur se connecte (le temps de réponse pour la tentative des attaquants tombera à 0). Vous pouvez également adapter l'algorithme de sorte que le délai est calculé sur la base du précédent délai et l'horodatage sur le fichier.
HTH
C.
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 horodateurs (ou les deux) des précédentes tentatives de connexion.
une vérification D'adresse IP peut être contournée si l'attaquant dispose de plus d'une adresse IP pour lancer ses requêtes à partir de la même adresse et peut être gênante si plusieurs utilisateurs se connectent à votre serveur à partir de la même adresse. Dans ce dernier cas, un défaut de connexion à plusieurs reprises empêcherait toute personne partageant la même adresse IP de en vous connectant avec ce nom d'utilisateur pour une certaine période de temps.
un contrôle d'horodatage a le même problème que ci-dessus: tout le monde peut empêcher tout le monde de se connecter à un compte particulier juste en essayant plusieurs fois. Utiliser un captcha au lieu d'une longue attente pour la dernière tentative est probablement une bonne solution.
les seules choses supplémentaires que le système de connexion devrait empêcher sont les conditions de course sur la fonction de vérification de tentative. Par exemple, dans la suite de pseudo-code
$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 des requêtes à la page de connexion? Il est probable que toutes les requêtes s'exécutent avec la même priorité, et il est probable qu'aucune requête ne parvienne à l'instruction increment_attempt_number avant que les autres n'aient franchi la deuxième ligne. Ainsi chaque requête reçoit la même valeur $time et $attempts et est exécutée. Prévenir ce type de problèmes de sécurité peut être difficile pour les applications complexes et implique le verrouillage et le déverrouillage de certaines tables/lignes de la base de données, bien sûr ralentir l'application.
la réponse courte est: Ne faites pas cela. Vous ne vous protégerez pas de la force brutale, vous pourriez même aggraver votre situation.
aucune des solutions proposées ne fonctionnerait. Si vous utilisez L'IP comme paramètre pour étrangler, l'attaquant va juste étendre l'attaque sur un grand nombre D'IPs. Si vous utilisez la session(cookie), l'attaquant va simplement laisser tomber les cookies. La somme de tout ce que vous pouvez penser est, qu'il n'y a absolument rien d'un attaquant de force brute n'a pas pu surmonter.
il y a une chose, cependant - vous vous fiez juste au nom d'utilisateur qui a essayé de se connecter. Donc, en ne regardant pas tous les autres paramètres que vous suivez combien de fois un utilisateur a essayé de se connecter et de mettre les gaz. Mais un agresseur veut te faire du mal. S'il le reconnaît, il ne fera que forcer les noms d'utilisateurs.
cela permettra à presque tous vos utilisateurs d'être étouffés à votre valeur maximale quand ils essaient de se connecter. Votre site web sera inutile. Attaquant: le succès.
vous pourriez retarder la vérification de mot de passe en général pour environ 200ms - l'utilisateur du site ne le remarquera presque pas. Mais un brute-forceur le fera. Cependant, rien de tout cela ne vous protégera du forçage Brutal ou DDoS - comme vous ne pouvez pas programmatically.
La seule façon de le faire est d'utiliser l'infrastructure.
vous devez utiliser bcrypt à la place de MD5 ou SHA-x pour hash vos mots de passe, cela rendra le décryptage de vos mots de passe beaucoup plus difficile si quelqu'un vole votre base de données (parce que je suppose que vous êtes sur un hôte partagé ou géré)
désolé de vous décevoir, mais toutes les solutions ici ont une faiblesse et il n'y a aucun moyen de les surmonter à l'intérieur de la logique d'arrière-plan.
cballuo a fourni une excellente réponse. Je voulais juste retourner la faveur en fournissant une version mise à jour qui soutient mysqli. J'ai légèrement modifié les colonnes table/field dans les sqls et d'autres petites choses, mais cela devrait aider quiconque cherche l'équivalent 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;
}
}
}
}