Comment créer une limitation de débit dans votre connexion Web App

Ce que vous allez créer

Bien que les rapports varient, le Washington Post a signalé que le récent piratage de photos de célébrités iCloud était centré sur le point de connexion non protégé de Find My iPhone:

"… Des chercheurs en sécurité auraient trouvé une faille dans la fonction Trouver mon iPhone de iCloud qui n'interrompait pas les attaques par force brute. La déclaration d'Apple… suggère que la société ne considère pas cette révélation comme un problème. Et c'est un problème, selon au chercheur en sécurité et contributeur du Washington Post, Ashkan Soltani.

Je suis d'accord. J'aurais aimé que Apple soit plus ouvert. sa réponse soigneusement formulée laissait place à différentes interprétations et semblait blâmer les victimes.

Les pirates ont peut-être utilisé ce script iBrute sur GitHub pour cibler les comptes de célébrités via Find My iPhone; la vulnérabilité a depuis été fermée.

Étant donné que l'une des sociétés les plus riches du monde n'a pas affecté les ressources nécessaires pour limiter tous ses points d'authentification, il est probable que certaines de vos applications Web n'incluent pas la limitation de taux. Dans ce tutoriel, je vais passer en revue quelques concepts de base de limitation de débit et une implémentation simple pour votre application Web basée sur PHP..

Comment fonctionnent les attaques de connexion

Les recherches effectuées dans le passé ont révélé des mots de passe que les gens ont tendance à utiliser le plus souvent. Xeno.net publie une liste des dix mille meilleurs mots de passe. Le graphique ci-dessous montre que la fréquence des mots de passe communs dans leur liste des 100 meilleurs utilisateurs est de 40% et que les 500 meilleurs constituent 71%. En d’autres termes, les utilisateurs utilisent et réutilisent généralement un petit nombre de mots de passe; en partie, parce qu'ils sont faciles à mémoriser et à taper.


Cela signifie que même une minuscule attaque par dictionnaire utilisant uniquement les vingt-cinq mots de passe les plus courants pourrait être assez efficace pour cibler des services.

Une fois qu'un pirate identifie un point d'entrée permettant un nombre illimité de tentatives de connexion, il peut automatiser les attaques par dictionnaire à grande vitesse et à volume élevé. S'il n'y a pas de limitation de débit, il devient facile pour les pirates informatiques d'attaquer avec des dictionnaires de plus en plus grands - ou des algorithmes automatisés avec un nombre infini de permutations..

En outre, si des informations personnelles sur la victime sont connues, par ex. Le nom de leur partenaire ou animal de compagnie actuel, un pirate informatique peut automatiser des attaques de permutations de mots de passe probables. Ceci est une vulnérabilité commune pour les célébrités.

Approches de la limitation de débit

Pour protéger les connexions, il existe plusieurs approches que je recommande comme base:

  1. Limiter le nombre de tentatives infructueuses pour un nom d'utilisateur spécifique
  2. Limiter le nombre de tentatives infructueuses par adresse IP

Dans les deux cas, nous voulons mesurer les tentatives infructueuses au cours d’une fenêtre ou de plusieurs fenêtres spécifiques, par exemple. 15 minutes et 24 heures.

Un des risques de blocage des tentatives par nom d'utilisateur est que l'utilisateur réel puisse se retrouver bloqué sur son compte. Nous voulons donc nous assurer que nous permettons à un utilisateur valide de rouvrir son compte et / ou de réinitialiser son mot de passe..

Les tentatives de blocage par adresse IP présentent le risque d’être partagées par de nombreuses personnes. Par exemple, une université peut héberger à la fois le titulaire du compte et une personne tentant de pirater son compte par malveillance. Bloquer une adresse IP peut bloquer le pirate informatique ainsi que l'utilisateur réel.

Cependant, un coût supplémentaire pour une sécurité accrue est souvent un inconvénient supplémentaire. Vous devez décider du taux de limitation de vos services et de la facilité avec laquelle les utilisateurs peuvent rouvrir leurs comptes..

Il peut être utile de coder une question secrète dans votre application, qui peut être utilisée pour réauthentifier un utilisateur dont le compte a été bloqué. Alternativement, vous pouvez envoyer une réinitialisation de mot de passe à leur email (en espérant que cela n'a pas été compromis).

Comment coder la limitation de débit

J'ai écrit un peu de code pour vous montrer comment limiter vos applications Web; mes exemples sont basés sur le framework Yii pour PHP. La plupart du code est applicable à toute application ou framework PHP / MySQL.

La table de connexion ayant échoué

Premièrement, nous devons créer une table MySQL pour stocker des informations sur les tentatives de connexion infructueuses. La table devrait stocker le adresse IP de l'utilisateur demandeur, le nom d'utilisateur ou l'adresse e-mail utilisé et un horodatage:

 $ this-> createTable ($ this-> tableName, array ('id' => 'pk', 'ip_address' => 'chaîne NOT NULL', 'nomutilisateur' => 'chaîne NOT NULL', 'created_at' => 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',), $ this-> MySqlOptions); 

Ensuite, nous créons un modèle pour la table LoginFail avec plusieurs méthodes: add, check et purge.

Enregistrement des tentatives de connexion infructueuses

En cas d'échec de la connexion, nous ajouterons une ligne à la table LoginFail:

 fonction publique add ($ username) // ajoute une ligne à la table de connexion ayant échoué avec le nom d'utilisateur et l'adresse IP $ failure = new LoginFail; $ échec-> nom d'utilisateur = $ nom d'utilisateur; $ failure-> ip_address = $ this-> getUserIP (); $ failure-> created_at = new CDbExpression ('NOW ()'); $ échec-> save (); // en cas d'échec de la connexion, purge l'ancien journal d'échec $ this-> purge ();  

Pour getUserIP (), J'ai utilisé ce code de Stack Overflow.

Nous pouvons également utiliser l'opportunité d'un login échoué pour purger la table des anciens enregistrements. Je le fais pour empêcher les contrôles de vérification de ralentir avec le temps. Ou, vous pouvez implémenter une opération de purge dans une tâche périodique en arrière-plan toutes les heures ou tous les jours:

public function purge ($ mins = 120) // purge les entrées de connexion ayant échoué avant $ mins $ minutes_ago = (time () - (60 * $ mins)); // par exemple. Il y a 120 minutes $ critères = new CDbCriteria (); LoginFail :: model () -> old_than ($ minutes_ago) -> applyScopes ($ criteres); LoginFail :: model () -> deleteAll ($ critères); 

Vérification des tentatives de connexion infructueuses

Le module d'authentification Yii que j'utilise ressemble à ceci:

fonction publique authenticate ($ attribut, $ params) if (! $ this-> hasErrors ()) // nous ne voulons authentifier que s'il n'y a pas d'erreur de saisie $ identity = new UserIdentity ($ this-> nom d'utilisateur, $ this-> mot de passe); $ identity-> authenticate (); if (LoginFail :: model () -> check ($ this-> nom d'utilisateur)) $ this-> addError ("nom d'utilisateur", UserModule :: t ("L'accès au compte est bloqué, contactez le support technique."));  else switch ($ identity-> errorCode) case UserIdentity :: ERROR_NONE: $ duration = $ this-> RememberMe? Yii :: app () -> contrôleur-> module-> RememberMeTime: 0; Yii :: app () -> user-> login ($ identity, $ duration); Pause; case UserIdentity :: ERROR_EMAIL_INVALID: $ this-> addError ("nom d'utilisateur", UserModule :: t ("Le courrier électronique est incorrect.")); LoginFail :: model () -> add ($ this-> nom d'utilisateur); Pause; case UserIdentity :: ERROR_USERNAME_INVALID: $ this-> addError ("username", UserModule :: t ("Le nom d'utilisateur est incorrect.")); LoginFail :: model () -> add ($ this-> nom d'utilisateur); Pause; case UserIdentity :: ERROR_PASSWORD_INVALID: $ this-> addError ("password", UserModule :: t ("Le mot de passe est incorrect.")); LoginFail :: model () -> add ($ this-> nom d'utilisateur); Pause; case UserIdentity :: ERROR_STATUS_NOTACTIV: $ this-> addError ("status", UserModule :: t ("Votre compte n'est pas activé.")); Pause; case UserIdentity :: ERROR_STATUS_BAN: $ this-> addError ("status", UserModule :: t ("Votre compte est bloqué.")); Pause; 

Chaque fois que mon code de connexion détecte une erreur, j'appelle la méthode pour ajouter des détails à ce sujet dans la table LoginFail:

LoginFail :: model () -> add ($ this-> nom d'utilisateur);

La section de vérification est ici. Cela s'exécute à chaque tentative de connexion:

$ identity-> authenticate (); if (LoginFail :: model () -> check ($ this-> nom d'utilisateur)) $ this-> addError ("nom d'utilisateur", UserModule :: t ("L'accès au compte est bloqué, contactez le support technique."));

Vous pouvez greffer ces fonctions sur la section d'authentification de connexion de votre propre code.

Mon contrôle de vérification recherche un grand nombre de tentatives de connexion infructueuses pour le nom d'utilisateur en question et séparément pour l'adresse IP utilisée:

 public function check ($ username) // vérifie si le seuil de connexion a échoué a été dépassé // pour le nom d'utilisateur au cours des 15 dernières minutes et de la dernière heure // et pour l'adresse IP au cours des 15 dernières minutes et de la dernière heure $ has_error = false; $ minutes_ago = (time () - (60 * 15)); // il y a 15 minutes $ hours_ago = (time () - (60 * 60)); // il y a 1 heure $ user_ip = $ this-> getUserIP (); if (LoginFail :: model () -> depuis ($ minutes_ago) -> nom d'utilisateur ($ nom d'utilisateur) -> count ()> = self :: FAILS_USERNAME_QUARTER_HOUR) $ has_error = true;  else if (LoginFail :: model () -> depuis ($ minutes_ago) -> ip_address ($ user_ip) -> count ()> = self :: FAILS_IP_QUARTER_HOUR) $ has_error = true;  else if (LoginFail :: model () -> depuis ($ hours_ago) -> nom d'utilisateur ($ username) -> compte ()> = self :: FAILS_USERNAME_HOUR) $ has_error = true;  else if (LoginFail :: model () -> depuis ($ hours_ago) -> ip_address ($ user_ip) -> count ()> = self :: FAILS_IP_HOUR) $ has_error = true;  if ($ has_error) $ this-> add ($ username); return $ has_error;  

Je vérifie les limites de taux pour les quinze dernières minutes ainsi que pour la dernière heure. Dans mon exemple, j'autorise 3 tentatives de connexion infructueuses par quinzaine et six par heure pour un nom d'utilisateur donné:

 const FAILS_USERNAME_HOUR = 6; const FAILS_USERNAME_QUARTER_HOUR = 3; const FAILS_IP_HOUR = 24; const FAILS_IP_QUARTER_HOUR = 12;

Notez que mes vérifications de vérification utilisent les portées nommées ActiveRecord de Yii pour simplifier le code de requête de base de données:

// étendue des lignes depuis la fonction publique timestamp depuis ($ tstamp = 0) $ this-> getDbCriteria () -> mergeWith (array ('condition' => '(UNIX_TIMESTAMP (created_at)>>. $ tstamp.') ')' ,)); return $ this;  // étendue des lignes avant la fonction publique timestamp old_than ($ tstamp = 0) $ this-> getDbCriteria () -> mergeWith (array ('condition' => '(UNIX_TIMESTAMP (created_at)<'.$tstamp.')', )); return $this;  public function username($username=")  $this->getDbCriteria () -> mergeWith (array ('condition' => '(nomutilisateur = "'. $ nomutilisateur. '")',)); return $ this;  fonction publique ip_address ($ ip_address = ") $ this-> getDbCriteria () -> mergeWith (array ('condition' => '(ip_address ="'. $ ip_address. '")', ')); return this ;

J'ai essayé d'écrire ces exemples pour pouvoir les personnaliser facilement. Par exemple, vous pouvez omettre les contrôles de la dernière heure et vous fier aux 15 dernières minutes. Vous pouvez également modifier les constantes pour définir des seuils supérieurs ou inférieurs pour le nombre de connexions par intervalle. Vous pouvez également écrire des algorithmes beaucoup plus sophistiqués. C'est à vous.

Avec cet exemple, pour améliorer les performances, vous souhaiterez peut-être indexer la table LoginFail par nom d'utilisateur et séparément par adresse IP..

Mon exemple de code ne modifie pas réellement le statut des comptes en comptes bloqués, ni ne fournit de fonctionnalité permettant de débloquer des comptes spécifiques, je vous laisse le soin. Si vous implémentez un mécanisme de blocage et de réinitialisation, vous souhaiterez peut-être proposer une fonctionnalité permettant de bloquer séparément par adresse IP ou par nom d'utilisateur..

J'espère que vous avez trouvé cela intéressant et utile. N'hésitez pas à poster des corrections, des questions ou des commentaires ci-dessous. Je serais particulièrement intéressé par les approches alternatives. Vous pouvez également me joindre sur Twitter @reifman ou m'envoyer un email directement.

Crédits: prévisualisation d'une photo d'iBrute via Heise Security