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:

<?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.
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.
@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?
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.
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.
Dus als ik het zo doe:

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.
Je weet wel dat je
if (count($results) == 1) {

doet en niet:
if (count($result) == 1) {
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.
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.
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.

Reageren