Login - Brute force protection - PDO

Overzicht Reageren

Sponsored by: Vacatures door Monsterboard

Top Low-Code Developer Gezocht!

Bedrijfsomschrijving Unieke Kansen, Uitstekende Arbeidsvoorwaarden & Inspirerend Team Wij zijn een toonaangevende, internationale organisatie die de toekomst van technologie vormgeeft door het creëren van innovatieve en baanbrekende oplossingen. Ons succes is gebaseerd op een hecht en gepassioneerd team van professionals die altijd streven naar het overtreffen van verwachtingen. Als jij deel wilt uitmaken van een dynamische, vooruitstrevende en inspirerende werkomgeving, dan is dit de perfecte kans voor jou! Functieomschrijving Als Low-Code Developer ben je een cruciaal onderdeel van ons team. Je werkt samen met collega's uit verschillende disciplines om geavanceerde applicaties te ontwikkelen en te optimaliseren met behulp van Low-code

Bekijk vacature »

Pagina: 1 2 volgende »

Tortuga web

tortuga web

09/08/2014 20:00:11
Quote Anchor link
Hallo,
Ik ben bezig een loginscript te updaten van mysqli naar PDO. Nu ben ik met behulp van het loginscript van Jeroen VD van 3 jaar geleden een heel eind gekomen, maar ik krijg de inlog-attempt-controle niet aan de praat.
Het script:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<?php
ini_set('display_errors',1); // 1 == on , 0 == off
error_reporting(E_ALL | E_STRICT);
session_start();  
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    $errors = array();
    if (trim($_POST['username']) == ''
            or (!preg_match('~^[0-9xyzXYZ][0-9]{7}[A-Za-z]$~',($_POST['username']))))
        $errors['username'] = 'Rellena número del NIE o NIF';
    if (trim($_POST['password']) == ''
            or (!preg_match('~^[0-9]{4}$~', $_POST['password'])))
        $errors['password'] = 'Rellena número socio.';
    if (!empty($_POST['name']))
         $errors['name'] = 'No valido';
     if (count($errors) == 0) {
     //Start procedure
     try {
         //Set maximum attempts to login om 3 within 5 minutes
          $maxAttempts = 3;
         $attemptsTime = 5;
         //Connect to the dBase
         require_once ('sql_link_sherpa.php');
          //Make the query
         $sql_select = "SELECT id, nombre, pass FROM socios WHERE dni = :dni";
          $userStmt = $db->prepare($sql_select);
          $userStmt->execute(array(
             ':dni' => $_POST['username']
             ));

         $results = $userStmt->fetchAll();
            
         //Get logintries for the ultimate 5 minutes
         $checkTries = "SELECT username FROM loginfail
            WHERE DateAndTime >= NOW() - INTERVAL :attemptsTime MINUTE
            AND username = :username
            GROUP BY username, IP
            HAVING (COUNT(username) = :maxAttempts)"
;
         $triesStmt = $db->prepare($checkTries);
         $triesStmt->execute(array(
                   ':username' => $_POST['username'],
               ':attemptsTime' => $attemptsTime,
               ':maxAttempts' => $maxAttempts
             ));
         $tries = $triesStmt->fetchAll();
            
         foreach ($results as $result) {
         //Check password and set session values            
              if (count($results) == 1 AND count($tries) <=3) {
            if (count($tries) > 3) {
                    header ('Refresh: 3; url=index.php');
                    echo 'Too many tries';
                    exit();
                }

            $hash = $result['pass'];
            if (!password_verify(($_POST['password']), $hash)) {
                $insertTry = "INSERT
                                           INTO loginfail (username, IP, DateAndTime)
                    VALUES ( :username, :IP, NOW())"
;
                $insertStmt = $db->prepare($insertTry);
                $insertStmt->execute(array(
                                  ':username' => $_POST['username'],
                          ':IP' => $_SERVER['REMOTE_ADDR']
                         ));

                header ('Refresh: 3; url=login.php');
                echo 'Numero incorrecto';
                exit();
            }

            else {
                $_SESSION['logged_in'] = TRUE;
                $_SESSION['id'] = $result['id'];
                $_SESSION['user'] = $result['nombre'];
                header("Location: index_socio.php");  
                exit();
            }
        }        
        
  }

  catch(PDOException $e) {
    $sMsg = '<p>
    Linenumber: '
.$e->getLine().'<br />
    File: '
.$e->getFile().'<br />
    Errormessage: '
.$e->getMessage().'
    </p>'
;
     trigger_error($sMsg);
  }
}
}

?>


Het script werkt voorzover als het de passwordcontrole betreft, de meldingen etc. worden correct weergegeven. Is het password fout, dan wordt dit in de database weggeschreven, dus ook hier geen probleem. Alleen krijg ik het niet werkend dat je na 3 pogingen binnen 5 minuten, geen inlogpoging meer kunt doen.
Iemand enig idee?

Excuus dat het script een beetje rommelig eruitziet wat betreft inspringingen. Dat komt door het knippen en plakken.
Gewijzigd op 09/08/2014 20:13:22 door Tortuga web
 
PHP hulp

PHP hulp

28/03/2024 22:29:20
 
Erwin H

Erwin H

09/08/2014 20:43:54
Quote Anchor link
Waarom ga je in dat stuk waar je de brute force probeert te detecteren twee queries draaien? Die eerst query is volgens mij volledig overbodig.
De tweede query levert volgens mij ook veel teveel rijen op. Het enige wat je volgens mij wilt weten is of er binnen de afgelopen tijd een bepaald aantal log in pogingen is geweest. Je kan eventueel meerdere verschillende pogingen tellen (bijvoorbeeld zelfde ip, zelfde username, of ip en username), maar je eindresultaat van die query zou gewoon 1 getal moeten zijn, namelijk het aantal pogingen. Met 1 simpele if kan je bepalen of er een brute force poging gedaan wordt.

Let overigens op dat dit een zeer gevaarlijke manier van checken is. Aan de ene kant is de kans klein dat je echt brute force pogingen detercteert, aan de andere kan is de kans groot dat je anderen uitsluit.
 
Tortuga web

tortuga web

09/08/2014 20:59:50
Quote Anchor link
@Erwin,
Dank voor je reactie, maar kun je misschien wat meer uitleg geven?
En waarom is dit geen veilige manier en hoe kun je een brute-force aanval wel het beste tegengaan?
 
Erwin H

Erwin H

09/08/2014 22:16:55
Quote Anchor link
Het probleem met brute force is dat dat veelal niet meer wordt gedaan op bijvoorbeeld 1 username, maar dat een reeks aan bekende passwords wordt geprobeerd op een reeks bekende (of ook onbekende) usernames. Dus als je het echt gaat loggen en dan per username, dan kan het zijn dat je op gegeven moment een hele reeks usernames hebt geblokkeerd. Ook krijg je bij het ip adres als snel problemen als er proxies tussenzitten. Ga je op ip adres blokkeren en blijkt dat het een proxy is, dan is iedereen achter die proxy meteen geblokkeerd.

Een alternatief is om brute force te frustreren. Veel wordt er gebruik gemaakt van langzame encryptie methodes, maar bijvoorbeeld ook gewoon een extra wachttijd inbouwen (middels sleep). Een extra wachttijd van bijvoorbeeld 2 seconde is niet echt merkbaar voor een normale gebruiker, maar voor iemand die duizenden inlogpogingen op je wil afvuren begint dat al snel heel vervelend te worden.
 
Tortuga web

tortuga web

09/08/2014 23:11:43
Quote Anchor link
Oke, ik snap wat je bedoelt, maar is die extra wachttijd niet juist wat Jeroen VD (http://www.phphulp.nl/php/script/beveiliging/login-bruteforce-protectie-pdo/1950/) probeert te berieken met zijn script?
Ik begrijp uit jouw reactie dat er veel slimmere en makkelijkere wegen zijn om dit te bereiken, heb je toevallig enig idee waar ik daarvan een voorbeeld, tutorial of zoiets kan vinden?
 
Erwin H

Erwin H

09/08/2014 23:17:21
Quote Anchor link
Ik zie nergens waar hij extra wachttijd erin bouwt. Als je het over die 5 minuten wachten hebt, nee, want dan krijg je dus de problemen die ik boven beschrijf.

Ik zou willen dat er slimmere en makkelijkere wegen zijn.... Een echt effectieve manier is er helaas niet. Je kan het proces alleen proberen te frustreren.
 
Tortuga web

tortuga web

09/08/2014 23:59:24
Quote Anchor link
Dus als ik het zo doe:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
foreach ($results as $result) {
    //Check    password and set session values            
    if (count($results) == 1) {
        $hash = $result['pass'];
        if (!password_verify(($_POST['password']), $hash)) {
            sleep(2);
            $insertTry = "INSERT INTO loginfail (username, IP, DateAndTime)
                    VALUES ( :username, :IP, NOW())";
            $insertStmt = $db->prepare($insertTry);
            $insertStmt->execute(array(
                        ':username' => $_POST['username'],
                        ':IP' => $_SERVER['REMOTE_ADDR']
                    ));
            header ('Refresh: 3; url=login.php');
            echo 'Numero incorrecto';
            exit();
        }
        else {
            $_SESSION['logged_in'] = TRUE;
            $_SESSION['id'] = $result['id'];
            $_SESSION['user'] = $result['nombre'];
            header("Location: index_socio.php");  
            exit();
        }
    }
    else {
        header ('Refresh: 3; url=login.php');
        echo 'DNI desconocido';
        exit();
    }    
}

Dan is het script vertraagd als het wachtwoord onjuist is.
Heb alleen nog wel een andere vraag, want de check functioneert wel helemaal goed als het wachtwoord niet klopt, maar als de DNI niet klopt, en dus (count($result) != 1), dan zou er de foutmelding moeten komen 'DNI desconocido', zoals de melding bij een onjuist wachtwoord, maar dit werkt niet en ik weet niet waarom niet.
 
Erwin H

Erwin H

10/08/2014 00:22:26
Quote Anchor link
Je weet wel dat je
if (count($results) == 1) {

doet en niet:
if (count($result) == 1) {
 
Tortuga web

tortuga web

10/08/2014 08:47:23
Quote Anchor link
Ja, want de $results (met s) is alles wat er uit de database opgehaald wordt, en omdat username uniek is, behoort, indien juist ingegeven, er 1 resultaat teruggegeven te worden. (Ik zie nu dat ik in de laatste zin van m´n laatste bericht foutief (count($result) != 1) heb geschreven, dat moet dus results zijn). Als er dus niet 1 resultaat teruggegeven wordt, moet dus de echo 'DNI desconocido' weergegeven worden, maar dat gebeurt niet.
Voor de zekerheid getest of de count dan niet met (count($result) == 1) zou moeten zijn, en vreemd genoeg krijg ik dan wel de melding 'DNI desconocido', OOK als de login gegevens juist ingevoerd worden, dus dat werkt ook niet.
 
Ward van der Put
Moderator

Ward van der Put

10/08/2014 09:48:46
Quote Anchor link
Met header('Refresh: 3; url=login.php') bereik je overigens ook een van de ongewenste effecten die Erwin eerder noemde: alle gewone gebruikers wachten de aangegeven 3 seconden, maar iemand die een aanval uitvoert, lapt die instructie "wacht 3 seconden tot de volgende inlogpoging" aan de laars. Dit zijn dus de punten waar sleep() of een vergelijkbare oplossing op zijn plaats is.
 
Tortuga web

tortuga web

10/08/2014 09:57:21
Quote Anchor link
Ja, dat begrijp ik. Die 3 in de refresh is voor mij tijdens het schrijven van het script, zodat ik de foutmelding kan lezen en de werking van het script kan testen. Voor waar Erwin op doelt, heb ik nu, direct achter de password verificatie de sleep(2) ingevoegd (zie scriptstukje hierboven). Is dit wél effectief zo??

En blijft het probleem dat als er geen resultaat uit de database teruggegeven wordt, er schijnbaar niks gebeurt, terwijl ik dan dus die laatste melding wil zien, maar dat werkt niet.
 
Ward van der Put
Moderator

Ward van der Put

10/08/2014 10:34:08
Quote Anchor link
Ja, zolang je eerst maar de databaseverbinding sluit, anders bouw je zelf een DDoS-aanval op "Too many connections".

Het is niet ongebruikelijk om de vertraging steeds langer te maken, bijvoorbeeld met 2 ^ x voor x inlogpogingen:

2 ^ 0 = 1 s
2 ^ 1 = 2 s
2 ^ 2 = 4 s
2 ^ 3 = 8 s
2 ^ 4 = 16 s
...

Verder moet je niet slechts naar losse inlogpogingen kijken, maar naar het grotere geheel. Daarvoor moet je de omvang van de site weten (of meten). Als je normaal gesproken bijvoorbeeld gemiddeld 1 login per 10 minuten hebt, weet je dat er iets loos is wanneer er plotseling 20 mislukte logins in 1 minuut zijn.
 
Erwin H

Erwin H

10/08/2014 10:40:52
Quote Anchor link
Tortuga web op 10/08/2014 08:47:23:
Ja, want de $results (met s) is alles wat er uit de database opgehaald wordt, en omdat username uniek is, behoort, indien juist ingegeven, er 1 resultaat teruggegeven te worden.

Zoals ik al eerder zei, ik vind die query om te beginnen vreemd, ik kan niet doorgronden waarom die nuttig is. Verder vind ik het ook vreemd dat je binnen een foreach loop, waarin je over de $results array loopt, een check op het aantal elementen in $results plaatst.
Ik zeg niet dat het allemaal fout is, maar ik begrijp je logica in elk geval niet.
 
Tortuga web

tortuga web

10/08/2014 11:04:46
Quote Anchor link
Dit is wat ik nu heb, ik heb de login-fail er volledig uitgehaald. En Erwin, ik ben het met je eens dat er iets niet helemaal logisch is, want omdat het resultaat altijd maar 1 rij op moet leveren, zou er geen foreach-loop in moeten zitten, maar die is er eigenlijk vanwege try and error ingeslopen. Het script werkte niet zonder en wel met, maar ik weet niet waarom.
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php
    ini_set('display_errors',1); // 1 == on , 0 == off
    error_reporting(E_ALL | E_STRICT);

    session_start();  

    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
        $errors = array();
        if (trim($_POST['username']) == ''    or (!preg_match('~^[0-9xyzXYZ][0-9]{7}[A-Za-z]$~',($_POST['username']))))
            $errors['username'] = 'Rellena número del NIE o NIF';
        if (trim($_POST['password']) == '' or (!preg_match('~^[0-9]{4}$~', $_POST['password'])))
            $errors['password'] = 'Rellena número socio.<br>Un número menos que 1000, empieza con 0 hasta que complete 4 dígitos.';
        if (!empty($_POST['name']))
            $errors['name'] = 'No valido';
        if (count($errors) == 0) {
            //Start procedure
            try {
                //Connect to the dBase
                require_once ('sql_link_sherpa.php');
                //Make the query
                $sql_select = "SELECT id, nombre, pass FROM socios WHERE dni = :dni";
                $userStmt = $db->prepare($sql_select);
                $userStmt->execute(array(
                                ':dni' => $_POST['username']
                            ));

                $results = $userStmt->fetchAll();
                $db = NULL;
                foreach ($results as $result) {
                    //Check    password and set session values            
                    if (count($results) == 1) {
                        $hash = $result['pass'];
                        if (!password_verify(($_POST['password']), $hash)) {
                            sleep(3);
                            header ('Refresh: 3; url=login.php');
                            echo 'Numero incorrecto';
                            exit();
                        }

                        else {
                            $_SESSION['logged_in'] = TRUE;
                            $_SESSION['id'] = $result['id'];
                            $_SESSION['user'] = $result['nombre'];
                            $_SESSION['start'] = time(); // Taking now logged in time.
                            // Ending a session in 30 minutes from the starting time.

                            $_SESSION['expire'] = $_SESSION['start'] + (30 * 60);
                            header("Location: index_socio.php");  
                            exit();
                        }
                    }

                    else {
                        header ('Refresh: 3; url=login.php');
                        echo 'NIF/NIE desconocido';
                        exit();
                    }    
                }
            }

            catch(PDOException $e) {
                $sMsg = '<p>
                        Linenumber: '
.$e->getLine().'<br />
                        File: '
.$e->getFile().'<br />
                        Errormessage: '
.$e->getMessage().'
                        </p>'
;
                trigger_error($sMsg);
            }
        }
    }

?>

@Ward, kun je misschien een tip van de sluier oplichten van hoe ik dat in mijn script implementeer?
Gewijzigd op 10/08/2014 11:08:51 door tortuga web
 
Erwin H

Erwin H

10/08/2014 11:17:17
Quote Anchor link
Als je zelf niet meer weet waarom je sommige regels code in je script hebt zitten dan wordt het tijd om je hele script opnieuw te schrijven. Of dan in elk geval het stuk waarin de regels zitten.
Ook wordt het dan tijd om serieus naar het commentaar in je script te kijken. Het enige wat je nu hebt is commentaar over wat je doet, maar dat is overbodig. Wat het doet is wel te zien, niet waarom je het doet. Commentaar is nodig om juist dat uit te leggen, en nu blijkt waarom. Ga ook niet denken 'dat komt later wel, ik wil het eerst werkend hebben'. Want dan ben je te laat. In de eerste plaats heb je er op dat moment geen zin meer in om het te doen, maar ook heb je al de helft van de momenten waarop je het commentaar juist nodig had al gehad. Dat blijkt nu maar weer.

Dus mijn tip: haal het hele stuk uit je script, ga opnieuw de logica bedenken, schrijf het opnieuw en bouw vanaf nu altijd direct commentaar in. Je hebt nu een duidelijk voorbeeld waarom je dat commentaar nodig hebt, dus elk argument dat je nu bedenkt om dit niet te doen is een slap excuus.
 
Tortuga web

tortuga web

10/08/2014 11:44:37
Quote Anchor link
Ik kan natuurlijk iedere keer als iets niet werkt, hier de vraag dumpen, zonder eerst zelf te zoeken, maar dat is mijn stijl niet. Dus zit er nu iets (wellicht) onlogisch in het script, maar dat is er met try and error ingekomen en zou ik graag willen weten hoe dat anders te doen.
Dus: Als ik de foreach-loop niet gebruik, dus zo:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
//Get the results from the database and close the connection
$result = $userStmt->fetchAll();
$db = NULL;
//Check the result from the database
if (count($result) == 1) {
     $hash = $result['pass'];
    if (!password_verify(($_POST['password']), $hash)) {
        sleep(3);
        header ('Refresh: 3; url=login.php');
        echo 'Numero incorrecto';
        exit();
    }

    else {
            $_SESSION['logged_in'] = TRUE;
        $_SESSION['id'] = $result['id'];
        $_SESSION['user'] = $result['nombre'];
        $_SESSION['start'] = time();
        $_SESSION['expire'] = $_SESSION['start'] + (30 * 60);
        header("Location: index_socio.php");  
        exit();
    }
}

else {
    header ('Refresh: 3; url=login.php');
    echo 'Username desconocido';
    exit();
}

?>

Dan wordt er (bij juiste inloggegevens) het password gecontroleerd en foutgemeld. Terwijl als ik de loop er wel in heb zitten, dan wordt de juiste controle uitgevoerd en foutgemeld als het password fout is en ingelogged als het goed is.
Wat dan niet werkt is de check op de username.
 
Erwin H

Erwin H

10/08/2014 11:55:33
Quote Anchor link
Waarom het nu fout gaat is dat je de array niet goed interpreteert. De array die je terug krijgt uit de fetchAll is een multidimensionale array van records uit de database. Die ziet er zo uit:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
Array
(
    [0] => Array
        (
            [id] => ...
            [nombre] => ...
            [pass] => ...
        )
)

$hash = $result['pass']; moet dus worden $hash = $result[0]['pass'];

Wel jammer dat je niet mijn advies uit de vorige post eerst hebt overgenomen.....
 
Tortuga web

tortuga web

10/08/2014 12:11:25
Quote Anchor link
Oké, dus de fout zit in de array in de array. Dat had ik niet begrepen, maar ik zie nu hoe het werkt.
Heb het in mijn script aangepast en nu werkt het wel goed. Dank je wel.
Wat denk je van de, wel heel simpele, toevoeging van de vertraging op een foute login, met de sleep() functie dus?
 
Erwin H

Erwin H

10/08/2014 12:15:10
Quote Anchor link
Ik zou die sleep veel eerder doen. Nu doe je het zo te zien alleen op een specifiek punt. Ik zou het gewoon aan het begin doen, zodat elke login poging wordt vertraagd. 2 of 3 seconden wachten is voor een gewone gebruiker geen probleem.
 
Tortuga web

tortuga web

10/08/2014 12:21:20
Quote Anchor link
Kun je uitleggen waarom je dat zou doen, ik zie dat niet helemaal. Als de login juist is hoeft er geen vertraging op te zitten om brute force te voorkomen, want dan ben je al binnen. En omdat dit script alleen een login bevat, is de password-check het punt waar opnieuw geprobeerd kan worden om in te loggen.
Of zie ik iets helemaal over het hoofd?
 
Ward van der Put
Moderator

Ward van der Put

10/08/2014 12:28:02
Quote Anchor link
Ja en nee. Als élke login, ook een geldige, 3 seconde op zich laat wachten, gaat daarvan al op voorhand een signaal uit: bij deze site gaat een brute force-aanval héél lang duren.
 

Pagina: 1 2 volgende »



Overzicht Reageren

 
 

Om de gebruiksvriendelijkheid van onze website en diensten te optimaliseren maken wij gebruik van cookies. Deze cookies gebruiken wij voor functionaliteiten, analytische gegevens en marketing doeleinden. U vindt meer informatie in onze privacy statement.