Så här skapar du gränsvärden för din webbappsinloggning

Vad du ska skapa

Medan rapporterna varierar rapporterade Washington Post att den senaste iCloud-kändisfotohackningen centrerades kring Find My iPhone: s oskyddade inloggningspunkt:

"... säkerhetsforskare sägs ha funnit en fel i iClouds Find My iPhone-funktion som inte avbröt våldsattacker. Apples uttalande ... föreslår att företaget inte betraktar den uppenbarelsen som ett problem. Och det är ett problem enligt till säkerhetsforskare och Washington Post-bidragsgivare Ashkan Soltani.

Jag håller med. Jag önskar att Apple hade varit mer framträdande; Dess omsorgsfullt formulerade svar lämnade rum för olika tolkningar och verkade skylda offren.

Hackare kan ha använt detta iBrute-skript på GitHub för att rikta kändiskonton via Hitta min iPhone; Sårbarheten har sedan dess stängts.

Eftersom en av de rikaste företagen i världen inte allokerade resurserna för att betygsätta gräns för alla deras autentiseringspunkter är det troligt att vissa av dina webbapps inte innehåller kursbegränsning. I den här handledningen går jag igenom några av de grundläggande begreppen för hastighetsbegränsning och en enkel implementering för din PHP-baserade webbapplikation.

Hur inloggningsattacker fungerar

Forskning från tidigare hack har utsatta lösenord som människor brukar använda oftast. Xeno.net publicerar en lista över de tio tusen bästa lösenorden. Deras diagram nedan visar att frekvensen av vanliga lösenord i deras topp 100 lista är 40% och topp 500 utgör 71%. Med andra ord använder folk och använder ofta ett litet antal lösenord; delvis, för att de är lätta att komma ihåg och lätt att skriva.


Det betyder att även en liten ordboksattack med bara de tjugosem vanligaste lösenorden kan vara ganska framgångsrik när man riktar in sig mot tjänster.

När en hackare identifierar en ingångspunkt som tillåter obegränsat inloggningsförsök kan de automatisera höghastighets-, högvolymeringsordböcker. Om det inte finns någon begränsningsbegränsning blir det lätt för hackare att attackera med större och större ordböcker - eller automatiserade algoritmer med oändligt antal permutationer.

Om personliga uppgifter om offret är känt, t.ex. deras nuvarande partner eller husdjurens namn kan en hackare automatisera attacker av permutationer av troliga lösenord. Detta är ett vanligt sårbarhet för kändisar.

Tillvägagångssätt för att begränsa priser

För att skydda loggar finns det ett antal tillvägagångssätt som jag rekommenderar som utgångspunkt:

  1. Begränsa antalet misslyckade försök för ett visst användarnamn
  2. Begränsa antalet misslyckade försök med IP-adress

I båda fallen vill vi mäta misslyckade försök under ett visst fönster eller i tidernas fönster, t.ex. 15 minuter och 24 timmar.

En risk för att blockera försök med användarnamn är att den faktiska användaren kan få låst ur sitt konto. Så vi vill se till att vi gör det möjligt för den giltiga användaren att öppna sitt konto igen och / eller återställa sitt lösenord.

En risk att blockera försök med IP-adress är att de ofta delas av många människor. Till exempel kan ett universitet vara värd för både den faktiska kontoinnehavaren och någon som försöker att skadligt hacka sitt konto. Blockering av en IP-adress kan blockera hackaren såväl som den faktiska användaren.

En kostnad för ökad säkerhet är emellertid ofta lite ökad olägenhet. Du måste bestämma hur strikt att betygsätta begränsa dina tjänster och hur enkelt du vill göra det för användarna att öppna om sina konton igen.

Det kan vara användbart att koda en hemlig fråga i din app som kan användas för att autentisera en användare vars konto blockerades. Alternativt kan du skicka en lösenordsåterställning till deras email (hoppas att det inte har äventyras).

Så här kodar du gränsvärden

Jag har skrivit lite kod för att visa hur du betygsätter gränser för dina webbapplikationer. Mina exempel är baserade på Yii Framework for PHP. Huvuddelen av koden är tillämplig på alla PHP / MySQL applikationer eller ramar.

Failed Login Table

Först måste vi skapa ett MySQL-bord för att lagra information från misslyckade inloggningsförsök. Bordet ska lagra IP-adress av den begärande användaren, försöket användarnamn eller e-postadress som används och en tidsstämpel:

 $ this-> createTable ($ this-> tableName, array ('id' => 'pk', 'ip_address' => 'sträng INTE NULL', 'användarnamn' => 'sträng INTE NULL', 'created_at' => "TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP",), $ this-> MySqlOptions); 

Sedan skapar vi en modell för LoginFail-tabellen med flera metoder: lägg till, kolla och rena.

Inspelning misslyckade inloggningsförsök

När det finns en felaktig inloggning lägger vi till en rad i LoginFail-tabellen:

 public function add ($ användarnamn) // lägg till en rad till den misslyckade inloggningstabellen med användarnamn och IP-adress $ failure = new LoginFail; $ fel-> användarnamn = $ användarnamn; $ failure-> ip_address = $ this-> getUserIP (); $ failure-> created_at = new CDbExpression ('NU ()'); $ Failure-> Spara (); // när det finns en felaktig inloggning, rena äldre fellogg $ this-> purge ();  

För getUserIP (), Jag använde den här koden från Stack Overflow.

Vi kan också använda möjligheten till en misslyckad inloggning, för att rensa tabellen över äldre poster. Det gör jag för att förhindra kontrollkontrollerna att sakta ner över tiden. Eller du kan genomföra en reningsoperation i en bakgrunds-cron-uppgift varje timme eller varje dag:

public function renge ($ mins = 120) // renning misslyckades inloggningsuppgifter äldre än $ minuter $ minutes_ago = (tid () - (60 * $ min)); // t.ex. 120 minuter sedan $ criteria = new CDbCriteria (); LoginFail :: model () -> older_than ($ minutes_ago) -> applyScopes ($ kriterier); LoginFail :: model () -> TaBortAlla ($ kriterier); 

Kontrollerar misslyckade inloggningsförsök

Den Yii-autentiseringsmodul som jag använder ser ut så här:

public function authenticate ($ attribut, $ params) if (! $ this-> hasErrors ()) // vi vill bara verifiera när inga inmatningsfel $ identity = new UserIdentity ($ this-> användarnamn, $ this-> Lösenord); $ Identitets-> autentisera (); if (LoginFail :: model () -> check ($ this-> användarnamn)) $ this-> addError ("användarnamn", UserModule :: t ("Kontoåtkomst är blockerad, kontakta support."));  annars switch ($ identity-> errorCode) fall UserIdentity :: ERROR_NONE: $ duration = $ this-> rememberMe? Yii :: app () -> controller-> modul-> rememberMeTime: 0; Yii :: app () -> user-> inloggning ($ identitet, $ varaktighet); ha sönder; case UserIdentity :: ERROR_EMAIL_INVALID: $ this-> addError ("användarnamn", UserModule :: t ("E-post är felaktigt.")); LoginFail :: model () -> Lägg till ($ this-> användarnamn); ha sönder; case UserIdentity :: ERROR_USERNAME_INVALID: $ this-> addError ("användarnamn", UserModule :: t ("Användarnamnet är felaktigt.")); LoginFail :: model () -> Lägg till ($ this-> användarnamn); ha sönder; case UserIdentity :: ERROR_PASSWORD_INVALID: $ this-> addError ("lösenord", UserModule :: t ("Lösenordet är felaktigt.")); LoginFail :: model () -> Lägg till ($ this-> användarnamn); ha sönder; case UserIdentity :: ERROR_STATUS_NOTACTIV: $ this-> addError ("status", UserModule :: t ("Ditt konto är inte aktiverat.")); ha sönder; case UserIdentity :: ERROR_STATUS_BAN: $ this-> addError ("status", UserModule :: t ("Ditt konto är blockerat.")); ha sönder; 

När min inloggningskod upptäcker ett fel, ringer jag metoden för att lägga till detaljer om den till LoginFail-tabellen:

LoginFail :: model () -> Lägg till ($ this-> användarnamn);

Verifieringsdelen är här. Detta körs med varje inloggningsförsök:

$ Identitets-> autentisera (); if (LoginFail :: model () -> check ($ this-> användarnamn)) $ this-> addError ("användarnamn", UserModule :: t ("Kontoåtkomst är blockerad, kontakta support."));

Du kan grafta dessa funktioner till din egen kods autentiseringssektion.

Min kontrollkontroll letar efter en hög volym misslyckade inloggningsförsök för användarnamnet i fråga och separat för den IP-adress som används:

 kontroll av allmän funktion ($ användarnamn) // kolla om misslyckat inloggströskel har kränkts // för användarnamn under senaste 15 minuter och sista timmen // och för IP-adress under de senaste 15 minuterna och sista timmen $ has_error = false; $ minutes_ago = (tid () - (60 * 15)); // 15 minuter sedan $ hours_ago = (tid () - (60 * 60)); // 1 timme sedan $ user_ip = $ this-> getUserIP (); om (LoginFail :: modell () -> sedan ($ minutes_ago) -> användarnamn ($ användarnamn) -> count ()> = själv :: FAILS_USERNAME_QUARTER_HOUR) $ has_error = true;  annars om (LoginFail :: modell () -> sedan ($ minutes_ago) -> ip_address ($ user_ip) -> count ()> = själv :: FAILS_IP_QUARTER_HOUR) $ has_error = true;  annars om (LoginFail :: modell () -> sedan ($ hours_ago) -> användarnamn ($ användarnamn) -> count ()> = self :: FAILS_USERNAME_HOUR) $ has_error = true;  annars om (LoginFail :: modell () -> sedan ($ hours_ago) -> ip_address ($ user_ip) -> count ()> = själv :: FAILS_IP_HOUR) $ has_error = true;  om ($ har_error) $ this-> add ($ användarnamn); returnera $ has_error;  

Jag kontrollerar räntegränserna för de senaste femton minuterna samt den sista timmen. I mitt exempel tillåter jag 3 misslyckade inloggningsförsök per femton minuter och sex per timme för ett visst användarnamn:

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

Observera att min verifieringskontroll använder Yii: s ActiveRecord-namngivna omfång för att förenkla databasen förfrågningskod:

// räckvidd rad eftersom tidsstämpel offentlig funktion sedan ($ tstamp = 0) $ this-> getDbCriteria () -> mergeWith (array ('condition' => '(UNIX_TIMESTAMP (created_at)>'. $ tstamp. ') ,)); returnera $ this;  // räckvidd rader före tidsstämpel offentlig funktion 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' => '(användarnamn = "'. $ användarnamn. '")')); returnera $ this;  public function ip_address ($ ip_address = ") $ this-> getDbCriteria () -> mergeWith (array ('villkor' => '(ip_address ="'. $ ip_address. ;

Jag har försökt skriva dessa exempel så att du enkelt kan anpassa dem. Till exempel kan du lämna kontrollerna för den sista timmen och förlita sig på det senaste 15-minutersintervallet. Alternativt kan du ändra konstanterna för att ange högre eller lägre tröskelvärden för antalet inloggningar per intervall. Du kan också skriva mycket mer sofistikerade algoritmer. Det är upp till dig.

Med detta exempel, för att förbättra prestanda, kan du indexera LoginFail-tabellen efter användarnamn och separat efter IP-adress.

Min provkod ändrar inte faktiskt status för konton för att blockera eller ger funktionalitet för att blockera specifika konton, jag lämnar det upp till dig. Om du genomför en blockerings- och återställningsmekanism kan du erbjuda funktionalitet för att blockera separat efter IP-adress eller användarnamn.

Jag hoppas att du har hittat det här intressant och användbart. Vänligen gärna posta rättelser, frågor eller kommentarer nedan. Jag skulle vara särskilt intresserad av alternativa tillvägagångssätt. Du kan också nå mig på Twitter @ reifman eller maila mig direkt.

Credits: iBrute förhandsgranskningsfoto via Heise Security