1 commenti my script, mysql, php

Gli aspetti da considerare nel valutare il livello di sicurezza dei sistemi di login sono molteplici e, alle volte, complessi.

In questa sede ho intenzione di soffermarmi su un aspetto specifico: sicurezza intrinseca delle password e attacchi brute force.

 

La questione può essere affrontata da due prospettive diverse: quella dell'utente, tenuto a scegliersi una password "sicura"; quella del programmatore che deve difendersi e tutelare i propri utenti da questo genere di minacce.

 

Cosa sono i brute force

I brute force sono software o applicativi che eseguono ripetuti tentativi di login provando a raffica una serie di password. Tali tentativi ripetuti si interrompono nel momento in cui la password viene individuata (tecnicamente detto exploit).

Ipotizzando che un brute force possa eseguire 100 tentativi al secondo ne deriva che essi sono in grado di eseguire ben 6000 tentativi al minuto e 360000 tentativi in un ora!!

 

A seconda della tipologia di parole utilizzate nei tentativi effettuati si possono distinguere diverse tipologie di attacco:

  1. parole composte da tutte le combinazioni possibili di lettere e/o numeri e di lunghezza crescente (brute force attak).
  2. tutte le parole di senso compiuto ricomprese in un dizionario composto da molte migliaia di parole (dictionary attak).
  3. parole comuni (common word); si basa sulla stessa logica del dictionary attack ma ricorre ad una raccolta di parole (anche prive di significato) che frequentemente sono impiegate dagli utenti per le password;
  4. conoscendo alcuni dati personali dell'utente vittima, l'attacco brute force viene eseguito tentando passwords associate ad informazioni del soggetto (password profiling): esempi classici sono il nome di uno dei familiari o le date di nascita.

 

Password sicura e usabilità

Il problema dei brute force deriva principalmente dal fatto che la pigrizia e la difficoltà a ricordare le password porta gli utenti a scegliere password semplici e, alle volte, sempre le stesse.

 

La questione è affrontata dal punto di vista dell'utente in un interessante articolo da cui ho preso impulso per scrivere questo post.

In particolare l'articolo citato affronta il problema dell'usabilità delle password e il loro livello di sicurezza dal punto di vista dell'utente. Il ragionamento seguito è volto a smentire (in parte) la convinzione diffusa secondo la quale una password, affinchè possa essere considerata sicura, debba essere necessariamente complessa e difficile.

 

Nel tentativo di avvalorare tale tesi, apparentemente paradossale, prende come riferimento un brute force che esegue 100 tentativi al secondo e calcola per ciascuna delle password selezionate come esempio il tempo che potenzialmente verrà impiegato dal brute force per individuarle (calcolo opinabile ma non è questo ciò che conta).

Il tempo stimato per l'individuazione della password andrà da pochi minuti (per le password estremamente semplici) fino ad arrivare a molti anni: ovviamente al crescere del tempo di individuazione della password ne deriva una maggiore sicurezza di quest'ultima.

 

Type Password Metodo Tempo di individuazione Livello di sicurezza
6 caratteri casuali jskerv Brute-force 1 mese rischiosa
6 caratteri casuali con numeri ergs43 Brute-force 8 mesi rischio basso
6 caratteri casuali, con maiuscole/minuscole e simboli j4fS<2 Brute-force 219 anni sicura a vita
6 caratteri, parola con significato comune orange Parole comuni 3 minuti molto rischiosa
6 caratteri, parola con significato non comune woosaa Dizionario 1 ora e 22 minuti molto rischiosa
2 parole con significato comune alpine fun Parole comuni 2 mesi rischio basso
3 parole con significato comune this is fun Parole comuni 2537 anni sicura per sempre

 

Dati alla mano, la conclusione a cui si perviene è che è più sicura una password formata da più parole di senso compiuto (una breve espressione semplice da memorizzare), piuttosto che una password complessa composta da caratteri alfanumerici, maiuscole/minuscole, simboli e priva di significato.

 

La questione dal punto di vista degli sviluppatori web

Gli attacchi brute force e, più in generale, la sicurezza delle password è una questione che investe non solo gli utenti ma, inevitabilmente, anche chi sviluppa applicazioni per il web. Come spesso accade, i metodi per affrontarla possono essere diversi.

 

In generale, vi è una doverosa responsabilità di chi è titolare di un sito web ad aver cura delle password dei propri utenti.

 

Occorre evidenziare, anche se non strettamente connessa all'argomento qui in oggetto, che alla luce della (cattiva) abitudine di utilizzare sempre la stessa password rende scorretto salvare le password in chiaro: da un lato vi sono motivazioni deontologiche dato che nemmeno i titolari dei siti devono poter conoscere le password dei propri utenti; dall'altro vi è un rischio, potenziale ma reale, che un hacker che riesca ad introdursi nel nostro database entri in possesso delle password "abituali" dei nostri utenti (la cui email è in chiaro solitamente).

Gli hash a cui si può ricorrere sono tanti, md5 o sha1, giusto per citare i più famosi (ma non i più sicuri). Questi è sempre opportuno impiegarli congiuntamente a dei salt.

 

Difendersi dai brute force

La soluzione che tradizionalmente viene adottata per difendersi da attacchi brute force consiste nel salvare nel database i tentativi falliti e impedire il login dopo un tot di tentativi errati.

Questo metodo è utilizzato dal plugin per Wordpress Login Lockdown.

 

Tali applicativi salvano all'interno di una tabella MySql il numero di tentativi falliti che si è provato ad effettuare con un determinato username e/o con un certo IP (che di per sè è un dato poco affidabile). Ad ogni tentativo fallito si incrementerà di +1 il campo INT che tiene traccia del numero di tentativi e, raggiunta una data soglia, si impedirà il login a quell'username e/o a quell'IP.

 

Per comprenderne la logica segnalo un tutorial scritto da Maurizio Tarchini su suo blog: http://www.mtxweb.ch/php_learn/?p=1071 (si consiglia di scaricare il suo framework StandardLib al cui interno è contenuta la classe bruteForcePrevention).

Il metodo impiegato blocca l'accesso per un certo numero di minuti ad un determinato IP.

 

Personalmente preferisco bloccare congiuntamente non solo l'IP ma anche l'username. Ho scritto una semplice classe che ritengo molto semplice e facile da implementare nei vari script di login.

 

Avremo la tabella locked_login in cui salveremo l'username per il quale il login è fallito, l'ip, la data in cui è stato effettuato il tentativo errato, il numero del tentativo.

CREATE TABLE IF NOT EXISTS `locked_login` (
  `id_lock` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL,
  `ip` varchar(255) NOT NULL,
  `data_fail` datetime NOT NULL,
  `num_fail` int(10) NOT NULL,
  PRIMARY KEY (`id_lock`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1

 

<?php
Class Locked_Login{
	/*setting*/
	protected $num_max_fails = 5; // numero massimo di fallimenti
	protected $minutes_lock = 30; // durata in minuti del lock
	/*var*/
	protected $username;
	protected $ip;
	
	public function __construct($username){
		$this->username = (string) $username;
		$this->ip = $this->get_ip();
		$this->clear_lock();
		}
	
	// metodo da eseguire per verificare se un utente è bloccato
	public function is_locked(){
		$query = "
				SELECT * FROM locked_login 
					WHERE 
						(username='" .mysql_real_escape_string($this->username). "' OR ip='" .mysql_real_escape_string($this->ip). "')
						AND
						num_fail>=".$this->num_max_fails." 
						AND
						data_fail>=DATE_SUB( NOW() , INTERVAL ".$this->minutes_lock." MINUTE )";
		$result = mysql_query($query) or die(mysql_error());
		if(mysql_num_rows($result)>=1){
			return TRUE;
			}
		return FALSE;
		}
	
	// metodo da eseguire in caso di login fallito
	public function add_lock(){
		$pre_query = "
						SELECT * FROM locked_login 
						WHERE 
							username='" .mysql_real_escape_string($this->username). "' OR 
							ip='" .mysql_real_escape_string($this->ip). "'";
		$pre_result = mysql_query($pre_query) or die(mysql_error());
		if(mysql_num_rows($pre_result)==0){
			$insert_lock = "
							INSERT INTO locked_login SET 
								username='" .mysql_real_escape_string($this->username). "', 
								ip='" .mysql_real_escape_string($this->ip). "',
								data_fail=NOW(),
								num_fail=1";
			mysql_query($insert_lock) or die(mysql_error());
			}
		else{
			$update_lock = "UPDATE locked_login SET
								data_fail=NOW(), 
								num_fail=num_fail+1
							WHERE
								username='" .mysql_real_escape_string($this->username). "' OR 
								ip='" .mysql_real_escape_string($this->ip). "'";
			mysql_query($update_lock) or die(mysql_error());
			}
		}
	
	private function clear_lock(){
		$query = "DELETE FROM locked_login WHERE data_fail<=DATE_SUB( NOW() , INTERVAL ".$this->minutes_lock." MINUTE )";
		mysql_query($query) or die(mysql_error());
		}
	
	// la funzione è tratta da 
	// http://roshanbh.com.np/2007/12/getting-real-ip-address-in-php.html
	public function get_ip(){
		if(!empty($_SERVER['HTTP_CLIENT_IP'])){
			$ip=$_SERVER['HTTP_CLIENT_IP'];
			}
		else if(!empty($_SERVER['HTTP_X_FORWARDED_FOR'])){
			$ip=$_SERVER['HTTP_X_FORWARDED_FOR'];
			}
		else{
			$ip=$_SERVER['REMOTE_ADDR'];
			}
		return $ip;
		}
	}	

 

Il costruttore richiede come dato l'username per il quale si effettua l'accesso. Dopodiche vi saranno due metodi pubblici:

  • is_locked() per verificare che un utente e/o IP ha raggiunto il numero massimo di tentativi (e quindi impedirgli l'esecuzione del login).
  • add_lock() che servirà per incrementare di 1 il campo num_fail (numero di fallimenti).

 

A fini puramente esemplificativi (con le specificità previste nel vostro sistema di login) l'applicazione delle classe sarà la seguente.

if(isset($_POST['submit'])){
	$user = $_POST['username'];
	$pass = $_POST['passowrd'];
	
	require_once('locked.class.php');
	$lock = New Locked_Login($user);
	
	if($lock->is_locked() == TRUE){ // se utente è lock
		echo 'user o ip lock';
		}
	else if(check_login($user, $pass) == FALSE){ // se login errato
		$lock->add_lock();
		echo 'Login errato';
		}
	else{ // se invece login esatto
		// creazione delle variabili di sessione
		// etc etc
		echo 'Login corretto';		
		}
	}

 

Utilizzo della funzione sleep per "rallentare" i brute force

Un altro metodo decisamente più semplice ma con una buona efficacia, applicabile in pochi e semplici passaggi, farà ricorso alla funzione sleep() di php: quest'ultima sospende l'esecuzione dello script per un certo numero di secondi.

 

Da un punto di vista descrittivo avremo.

if(isset($_POST['submit'])){
	$user = $_POST['username'];
	$pass = $_POST['passowrd'];
	
	sleep(3); // sospendiamo l'esecuzione delle script
	
	if(check_login($user, $pass) == FALSE){ // se login errato
		echo 'Login errato';
		}
	else{ // se invece login esatto
		// creazione delle variabili di sessione
		// etc etc
		echo 'Login corretto';		
		}
	}

 

Prima di eseguire la/le query per verificare le credenziali sospendiamo l'esecuzione dello script per un certo numero di secondi, nell'esempio 3 secondi, rendendo la vita molto difficile per un brute force: si dovrà attendere 3 secondi per conoscere l'esito del tentativo; un brute force potrà così effettuare 20 tentativi al minuto, 1200 in un ora, che sono molto pochi rispetto ai 6000 e 360000 di cui si accennava poco sopra.

 

Certo, si tratta di una soluzione "rozza", soprattutto agli occhi di coloro che sono attenti all'ottimizzazione dei tempi di esecuzione degli script e badano ai centesimi di secondo. Ma, personalmente, lo ritengo il prezzo minimo da pagare per una soluzione tanto efficace quanto semplice da implementare.

 

Affrontare il problema a monte: validazione delle password

Come detto in precedenza una password troppo semplice è "facile" da indovinare. Alle volte potrebbero bastare anche pochissimi tentativi fatti manualmente per poter indovinare la password, ad esempio nel caso della data di nascita.

 

Per tali ragioni in sede di registrazione si potrebbe obbligare l'utente a scegliersi una password che abbia un livello di sicurezza accettabile.

Tale verifica dovrà riguardare almeno la lunghezza, assolutamente maggiore di 7 caratteri (meglio ancora 8) effettuando un banale controllo con strlen().

<?php
$password = "DemoPass11";

if (strlen($password)>=8) {
    echo "Password sicura.";
	} 
else {
    echo "Password poco sicura.";
	}
?>

 

Se si vuole essere più esigenti si potrà vincolare anche la scelta dei caratteri utilizzati. A titolo di esempio riporto una semplice funzione per validare le password che obbliga ad scegliere una password di almeno 8 caratteri e che contenga numeri e lettere maiuscole e minuscole.

<?php
$password = "DemoPass11";

if (preg_match("/^.*(?=.{8,})(?=.*d)(?=.*[a-z])(?=.*[A-Z]).*$/", $password)) {
    echo "Password sicura.";
	} 
else {
    echo "Password poco sicura.";
	}
?>

 

Per i metodi per eseguire la validazione anche lato client e per ulteriori dettagli sui metodi di validazione si rimanda a questo articolo.

 

Occorre notare che questi pattern di validazione non renderebbero valide alcune delle password che nell'articolo prima citato venivano considerate sicure.

 

Volendo, invece, seguire la logica descritta nell'articolo precedente un metodo di validazione diverso potrebbe verificare che la passowrd sia lunga più di 9 caratteri (compresi gli spazi) e/o contiene spazi. In genere non consigliabile come metodo dato che potrebbe disorientare l'utente.

 

E' opportuno, infatti, evidenziare che sistemi di validazione di questo tipo comportano un disagio per l'utente forzato a scegliere una password in maniera insolita.

 

Conclusioni

Il problema è complesso, anche al di là delle mie personali conoscenze, e quanto detto non è esaustivo.

Tanto per dire non si è assolutamente accennato allo sniffing delle password. 

 

Come giudicate i sistemi che ho suggerito? Quali altri sistemi suggerite di adottare per difendere le password? Avete avuto diretta esperienza con questo tipo di attacchi? Se si con quali conseguenze e/o rimedi?  

Olimpio Romanella

Sono un appassionato di Web Developing con un particolare debole per php. Mi dedico principalmente dello sviluppo back-end ed in particolare programmazione lato server con php, sviluppo di database relazionali MySql e progettazione di CMS di piccole e medie dimensioni.

Mi avvalgo del framework javascript Jquery, utilizzando molti dei suoi plugin e nei dei miei progetti utilizzo spesso il framework MVC Codeigniter.

1 Commenti presenti

avatar Nicola Iarocci

Nicola Iarocci

11 May 2011 ore 15:11

Ciao, bell'articolo. Ti segnalo la traduzione italiana autorizzata dell'articolo di Thomas, che ho pubblicato proprio stamattina. Magari può interessare i tuoi lettori :)