Cum se construiește limitarea ratei în conectarea la aplicația Web

Ce veți crea

În timp ce rapoartele variază, The Washington Post a raportat că recentul iCloud hacking de fotografie celebră a fost centrat în jurul punctului de autentificare neprotejat My iPhone:

"... cercetătorii din domeniul securității s-au declarat că au găsit un defect în caracteristica Find My iPhone a lui iCloud, care nu a întrerupt atacurile de forță brute." Declarația Apple ... sugerează că compania nu consideră că revelația este o problemă. la cercetătorul de securitate și contributor la Washington Post Ashkan Soltani.

Sunt de acord. Mi-aș fi dorit ca Apple să fi fost mai apropiată; răspunsul său bine formulat a lăsat loc pentru interpretări diferite și părea să dea vina pe victime.

Hackerii au folosit acest script iBrute pe GitHub pentru a-și viza conturile de celebritate prin Find My iPhone; vulnerabilitatea a fost închisă de atunci.

Întrucât una dintre cele mai bogate corporații din lume nu a alocat resursele pentru a limita toate punctele lor de autentificare, este probabil ca unele dintre aplicațiile dvs. web să nu includă limitarea ratei. În acest tutorial, voi trece prin câteva dintre conceptele de bază privind limitarea ratei și o simplă implementare pentru aplicația web bazată pe PHP.

Cum funcționează Atacurile de conectare

Cercetarea din hack-uri anterioare a expus parolele pe care oamenii tind să le utilizeze cel mai frecvent. Xeno.net publică o listă cu primele zece mii de parole. Diagrama de mai jos arată că frecvența parolelor comune în lista lor de top 100 este de 40%, iar primele 500 reprezintă 71%. Cu alte cuvinte, utilizatorii folosesc frecvent și reutilizează un număr mic de parole; în parte, deoarece sunt ușor de memorat și ușor de scris.


Asta înseamnă că chiar și un mic atac de dicționar care folosește doar cele douăzeci și cinci de parole cele mai comune ar putea fi destul de reușit atunci când vizează serviciile.

Odată ce un hacker identifică un punct de intrare care permite tentative nelimitate de conectare, aceștia pot automatiza atacurile de dicționare de mare viteză și volum mare. Dacă nu există nici o limitare a ratei, atunci devine ușor pentru hackeri să atace cu dicționare mai mari și mai mari - sau algoritmi automatizați cu număr infinit de permutări.

Mai mult, dacă sunt cunoscute informații personale despre victimă, de ex. partenerul lor actual sau numele animalului de companie, un hacker poate automatiza atacurile de permutări ale unor parole posibile. Aceasta este o vulnerabilitate comună pentru celebrități.

Abordări privind limitarea ratei

Pentru a proteja datele de conectare, există câteva abordări pe care le recomand ca bază:

  1. Limitați numărul de încercări eșuate pentru un anumit nume de utilizator
  2. Limitați numărul de încercări eșuate prin adresa IP

În ambele cazuri, dorim să măsuram încercările eșuate în timpul unei anumite ferestre sau ferestre de timp, de ex. 15 minute și 24 de ore.

Unul dintre riscurile încercărilor de blocare după numele de utilizator este că utilizatorul real ar putea fi blocat din contul său. Deci, vrem să ne asigurăm că utilizatorul valid va putea să-și redeschidă contul și / sau să-și reinițializeze parola.

Un risc pentru încercările de blocare prin adresa IP este că acestea sunt adesea împărtășite de mulți oameni. De exemplu, o universitate ar putea găzdui atât titularul propriu-zis al contului, cât și pe cineva care încearcă să-și compromită contul. Blocarea unei adrese IP poate bloca hackerul, precum și utilizatorul real.

Cu toate acestea, un cost pentru o securitate sporită este adesea un pic de inconvenient crescut. Trebuie să decideți cât de strict să ratați să limitați serviciile și cât de ușor doriți să le faceți utilizatorilor să-și redeschidă conturile.

Poate fi util să codificați o întrebare secretă în aplicația dvs., care poate fi utilizată pentru a re-autentifica un utilizator al cărui cont a fost blocat. Alternativ, puteți trimite o resetare a parolei la e-mailul lor (sperând că nu a fost compromis).

Modul de limitare a ratei de cod

Am scris un pic de cod pentru a vă arăta cum să evaluați limita aplicațiilor web; exemplele mele se bazează pe cadrul Yii pentru PHP. Majoritatea codului se aplică oricărei aplicații sau a cadrului PHP / MySQL.

Tabela de autentificare a eșuat

În primul rând, trebuie să creați un tabel MySQL pentru a stoca informații din încercările de conectare eșuate. Tabelul ar trebui să stocheze adresa IP a utilizatorului solicitant, a tentativului de utilizator sau a adresei de e-mail utilizate și o marcaj de timp:

 $ this-> createTable ($ this-> numeNumar, array ('id' => 'pk', 'ip_address' => 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP PE UPDATE CURRENT_TIMESTAMP',) $ this-> MySqlOptions); 

Apoi, creăm un model pentru tabelul LoginFail cu mai multe metode: adăugați, verificați și eliminați.

Înregistrarea nu a reușit

Ori de câte ori există o conectare nereușită, vom adăuga un rând în tabelul LoginFail:

 funcția publică adăugă ($ username) // adăuga un rând la tabela de autentificare eșuată cu nume de utilizator și adresa IP $ fail = new LoginFail; $ eșec-> username = $ username; $ eșec-> ip_address = $ this-> getUserIP (); $ fail-> created_at = nou CDbExpression ('ACUM ()'); $ Failure-> Salvare (); // ori de câte ori există o conectare nereușită, eliminați jurnalul de erori vechi $ this-> purge ();  

Pentru getUserIP (), Am folosit acest cod din suprapunerile de stive.

De asemenea, putem folosi ocazia unei autentificări nereușite, pentru a elimina tabelul înregistrărilor mai vechi. Fac acest lucru pentru a împiedica încetinirea verificărilor de verificare în timp. Sau, puteți implementa o operație de purjare într-o sarcină cron de fundal în fiecare oră sau în fiecare zi:

funcția de purjare a funcțiilor publice ($ mins = 120) // purjare nu a reușit să înregistreze intrările mai vechi de $ mins $ minutes_ago = (time () - (60 * $ mins)); // de exemplu. Acum 120 minute $ criteria = noi CDbCriteria (); LoginFail :: model () -> older_than ($ minutes_ago) -> applyScopes ($ criterii); LoginFail :: model () -> deleteAll ($ criterii); 

Verificarea încercărilor de conectare eșuată

Modulul de autentificare Yii pe care îl folosesc arată astfel:

($ this-> hasErrors ()) // dorim doar sa ne autentificam atunci cand nu exista erori de intrare $ identity = new UserIdentity ($ this-> username, $ this-> parola); $ Identitate-> autentifice (); dacă (LoginFail :: model () -> verificați ($ this-> username)) $ this-> addError ("username", UserModule :: t ("Accesul la cont este blocat;  altceva switch ($ identitate-> errorCode) caz UserIdentity :: ERROR_NONE: $ duration = $ this-> rememberMe? Yii :: app () -> controler-> modul-> rememberMeTime: 0; Yii :: app () -> user-> autentificare ($ identitate, $ durată); pauză; caz UserIdentity :: ERROR_EMAIL_INVALID: $ this-> addError ("nume de utilizator", UserModule :: t ("Emailul este incorect.")); LoginFail :: model () -> adaugă ($ this-> nume de utilizator); pauză; caz UserIdentity :: ERROR_USERNAME_INVALID: $ this-> addError ("nume de utilizator", UserModule :: t ("Numele de utilizator este incorect.")); LoginFail :: model () -> adaugă ($ this-> nume de utilizator); pauză; caz UserIdentity :: ERROR_PASSWORD_INVALID: $ this-> addError ("parola", UserModule :: t ("Parola este incorectă")); LoginFail :: model () -> adaugă ($ this-> nume de utilizator); pauză; caz UserIdentity :: ERROR_STATUS_NOTACTIV: $ this-> addError ("status", UserModule :: t ("Contul dvs. nu este activat")); pauză; caz UserIdentity :: ERROR_STATUS_BAN: $ this-> addError ("status", UserModule :: t ("Contul dvs. este blocat")); pauză; 

Ori de câte ori codul meu de conectare detectează o eroare, am sunat la metoda pentru a adăuga detalii despre aceasta în tabelul LoginFail:

LoginFail :: model () -> adaugă ($ this-> nume de utilizator);

Secțiunea de verificare este aici. Aceasta se întâmplă cu fiecare încercare de conectare:

$ Identitate-> autentifice (); dacă (LoginFail :: model () -> verificați ($ this-> username)) $ this-> addError ("username", UserModule :: t ("Accesul la cont este blocat;

Puteți altoi aceste funcții în secțiunea de autentificare a codului propriu.

Verificarea mea de verificare caută un volum mare de încercări de conectare eșuate pentru numele de utilizator în cauză și separat pentru adresa IP utilizată:

 ($ username) // verifica dacă pragul de autentificare eșuat a fost încălcat // pentru numele de utilizator în ultimele 15 minute și ultima oră // și pentru adresa IP în ultimele 15 minute și ultima oră $ has_error = false; $ minutes_ago = (timp () - (60 * 15)); // 15 minute în urmă $ hours_ago = (time () - (60 * 60)); // 1 oră în urmă $ user_ip = $ this-> getUserIP (); dacă (LoginFail :: model () -> din ($ minutes_ago) -> nume utilizator ($ username) -> count ()> = auto :: FAILS_USERNAME_QUARTER_HOUR) $ has_error = true;  altfel dacă (LoginFail :: model () -> din ($ minutes_ago) -> ip_address ($ user_ip) -> count ()> = auto :: FAILS_IP_QUARTER_HOUR) $ has_error = true;  altfel dacă (LoginFail :: model () -> din ($ hours_ago) -> nume de utilizator ($ username) -> count ()> = auto :: FAILS_USERNAME_HOUR) $ has_error = true;  altfel dacă (LoginFail :: model () -> din ($ hours_ago) -> ip_address ($ user_ip) -> count ()> = auto :: FAILS_IP_HOUR) $ has_error = true;  dacă ($ has_error) $ this-> add ($ username); întoarcere $ has_error;  

Am verificat limitele ratei pentru ultimele cincisprezece minute, precum și pentru ultima oră. În exemplul meu, permiteți 3 încercări de conectare eșuate la fiecare cincisprezece minute și șase pe oră pentru orice nume de utilizator dat:

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

Rețineți că verificările mele de verificare utilizează domeniile numite ActiveRecord numite pentru simplificarea codului interogării bazei de date:

// domeniu de randuri de la functionarea publica a timestampului de la ($ tstamp = 0) $ this-> getDbCriteria () -> mergeWith (array ('condition' => ,)); returnați $ this;  // domeniul de aplicare al rândurilor înainte de funcția publică timestamp older_than ($ tstamp = 0) $ this-> getDbCriteria () -> mergeWith (array ('condition' => '(UNIX_TIMESTAMP (created_at)<'.$tstamp.')', )); return $this;  public function username($username=")  $this->getDbCriteria () -> mergeWith (array ('condition' => '(username =' '. $ username.' ')';)); returnați $ this;  funcția publică ip_address ($ ip_address = ") $ this-> getDbCriteria () -> mergeWith (array ('condition' => 'ip_address ="' $ ip_address. ;

Am încercat să scriu aceste exemple pentru a le personaliza cu ușurință. De exemplu, ați putea lăsa cecurile în ultima oră și să vă bazați pe ultimul interval de 15 minute. Alternativ, ați putea modifica constantele pentru a stabili praguri mai mari sau mai mici pentru numărul de intrări pe interval. S-ar putea scrie și algoritmi mult mai sofisticați. Depinde de tine.

Cu acest exemplu, pentru a îmbunătăți performanța, vă recomandăm să indexați tabelul LoginFail după nume de utilizator și separat prin adresa IP.

Codul meu de probă nu schimbă starea conturilor în mod blocat sau oferă funcționalități pentru deblocarea anumitor conturi, vă voi lăsa la latitudinea dvs. Dacă implementați un mecanism de blocare și resetare, vă recomandăm să oferiți funcționalitate pentru a bloca separat adresa IP sau numele de utilizator.

Sper că ați găsit acest lucru interesant și util. Nu ezitați să postați corecturi, întrebări sau comentarii de mai jos. Mi-ar fi interesat în special abordările alternative. Puteți să mă contactați și pe Twitter @reifman sau să mă trimiteți direct prin e-mail.

Credite: iBrute previzualizare fotografie prin Heise Securitate

Cod