Lunedì, 25 Febbraio 2019 23:53

Il login debole e forte in PHP con le funzioni di hash

Scritto da

Negli esempi iniziali visti col PHP e il nostro database, le password compaiono sempre in chiaro come campi di testo all'interno delle nostre tabelle di anagrafica. Questa è una pratica sconsigliata nel mondo reale. Abbiamo gia visto che una debolezza nei controlli SQL Injection permetterebbe di visualizzare dati sensibili e le password sono estremamente importanti! Più in generale, un backup sbagliato, un occhio non autorizzato al nostro database sarebbe pericolosissimo. Ma del resto non serve a nessuno che quelle password siano salvate in chiaro, a noi basta una sua versione incenerita!

No, non è un errore di battitura. Qualsiasi documento, sequenza di caratteri più o meno lunga, dal libro fino ad una password di pochi caratteri possono essere passate attraverso una funzione detta appunto di hash che crea un'impronta, un digest univoco del nostro testo e che non può essere soggetto a reverse engineering. In figura, tratta da wikipedia, un esempio di come si comporta un funzione di hash. In questo modo, essendo password->sua impronta una relazione univoca, sul database non salviamo la password in chiaro, ma la sua impronta che non può essere decriptata ma può essere usata per controllare che, ogni volta che un utente chiede di entrare con quella password, che l'impronta della password inserita sia uguale a quella presente nel database.

(Funzionamento di una funzione di hash, immagine da Wikipedia)

Gli algoritmi di hashing sono molti, alcuni più performanti, altri meno, ma diciamo che agli occhi dell'utente medio, è difficile che si riscontrino differenze. Tra i più utilizzati ci sono MD5 e SHA1, anche se non considerati più del tutto affidabili, conviene spesso sostituirli con i più recenti SHA2 come SHA512 o tecniche miste che sfruttano hash di altri hash a cascata.

Vediamo ora un esempio pratico e semplice di login in PHP.

Il primo frammento, è una semplice pagina HTML con la form di login user/password. Possiamo chiamarlo con login.html

<!DOCTYPE html>
<html lang="it">
<head>
    <title>Login</title>
    <meta charset="utf-8">
</head>

<body>
<form name="logindebole" action="login.php" method="post">
    <fieldset>
        <legend>Login</legend>
            <label for="user">User:</label><br>
            <input type="text" name="user"> <br>
            <label for="password">Password:</label><br>
            <input type="password" name="password">
            <br><br>   
            <input type="submit" value="INVIA">
    </fieldset>
</form>
</body>
</html>

 Un primo esempio di script login.php scatenato dalla form precedente potrebbe essere il seguente. Il caso trattato è estremo: un solo utente e il controllo è immerso nel codice PHP con tanto di password in chiaro per la gioia di qualche sistemista malevolo o di un hacker che riesce ad accedere al file fisico, e non è una possibilità remota. Un caso del tutto inefficiente ma tanto vale vederlo al  volo:

<?php
		
session_start();
if(isset($_POST['password']) &&  isset($_POST['user']) && 
	 $_POST['password'] == '123456' && $_POST['user'] == 'pollo') 
{
	$_SESSION['logged'] = true;
	$_SESSION['logged_user'] = 'Ing. Pollo';
}

if(isset($_SESSION['logged']) && $_SESSION['logged'] == true )
{
  echo "Sei collegato come ", $_SESSION['logged_user'];
  echo "Sei autorizzato, ad esempio, a scaricare questo file o visualizzare un link o testo ecc" 
}
else
{
  echo "Autenticazione Fallita";
}

echo "<br>", "Il tuo id di sessione e': " . session_id(); 
?>

Il caso è banale ma evidenzia la criticità: la password o le password degli utenti vengono immerse nel codice. In questo modo qualunque sviluppatore che metta le mani su codice dopo di noi potrebbe visualizzare le informazioni sensibili dell'utente ed, inoltre, la soluzione proposta non sarebbe dinamica/scalabile perché non potremmo aggiungere in modo agevole un altro utente se non modificando il codice. Vogliamo realizzare un codice che permetta agli utenti di inserire la propria password ma senza che sviluppatori o amministratori server/database possano in alcun modo vedere in chiaro la password inserita. Proviamo la stessa situazione con password ma questa volta su un database con una opportuna tabella degli utenti.

In questa esercitazione ci eravamo già creati una database con una tabella per gestire gli utenti. Potete usare qualcosa simile al frammento di codice qui sotto

CREATE TABLE `users` (
  `username` varchar(64) NOT NULL,
  `password` varchar(128) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Come si vede, la password altri non è che una stringa di testo "in chiaro", ancora. Certo il db ci permette di avere un numero di utenti scalabile senza necessariamente modificare il codice. Ma col codice scritto così, si presenta il rischio di SQL injection, che permetterebbe di visualizzare le password in chiaro o banalmente, un sistemista o chiunque riesca ad accedere al db, riuscirebbe a vedere una informazione così sensibile come la password. Ho messo preventivamente un VARCHAR di 128 caratteri per la password, il motivo ci sarà chiaro tra poco. Riscriviamo intanto il precedente file login.php. Il file richiesto config.php altro non è che la inizializzazione  delle variabili necessarie alla connessione del db.

<?php
require_once("config.php");

$connessione = mysqli_connect($mysql_host,$mysql_user,$mysql_pass,$mysql_db);
if(mysqli_connect_errno())
  die("Connessione non riuscita: " . mysqli_error($connessione));

if (isset($_POST['password']) &&  isset($_POST['user']))
{
  $user = $_POST['user'];
  //$passwordhash = sha512($password); //in molti wamp/apache server non è supportata per limiti di memoria
  $passwordhash = hash("sha512", $password);
  $query = "SELECT * FROM users 
            WHERE username = '$user' 
			  AND password = '$passwordhash'";
  $res = mysqli_query($connessione,$query) or die("Errore nella login: " . mysqli_error($connessione));
  if ($res) echo "Ciao utente loggato!";
  else echo "Errore: " . $query . "<br>" . $mysqli_error($connessione);
}
$mysqli_close($connessione);

?>

A questo punto, visti i pericoli del login debole, vediamo come sfruttare le funzioni di hash per salvare sul db non la password ma il suo digest e controllarlo poi in fase di login. Ovviamente cominciamo con lo scrivere una ipotetica form di iscrizione dell'utente al nostro sito. Creiamo uno script semplice html che possiamo chiamare creautente.html

<!DOCTYPE html>
<html lang="it">
<head>
    <title>Registrazione</title>
    <meta charset="utf-8">
</head>

<body>
<form name="creautente" action="creautente.php" method="post">
  <fieldset>
    <legend>Registrazione</legend>
    <div class="labelf">
      <label for="user">User:</label>
    </div>
    <div class="inputf">
    <input type="text" name="user" required>
    </div>
    <br>
    <div class="labelf">
      <label for="password">Password:</label>
    </div>
    <div class="inputf">
      <input type="password" name="password" required>
    </div>
    <br>
    <input type="submit" value="REGISTRATI">
  </fieldset>
</form>
</body>
</html>

La form passa i suoi dati allo script PHP creautente.php sotto riportato.

 

<?php
require_once("config.php");

$connessione = mysqli_connect($mysql_host,$mysql_user,$mysql_pass,$mysql_db);
if (mysqli_connect_errno())
  die("Connessione non riuscita: " . mysqli_error($connessione));

if (isset($_POST['password']) &&  isset($_POST['user']))
{
  $user = $_POST['user'];
  $password = $_POST['password'];
  //$passwordhash = sha512($password); //in molti wamp/apache server non è supportata per limiti di memoria
  $passwordhash = hash("sha512", $password);
  $query = "INSERT INTO users (username, password)
            VALUES ('$user','$passwordhash')";
  $res = mysqli_query($connessione, $query) or die("Errore nella creazione dell'utente: " . mysqli_error($connessione));

  if ($res) echo "Nuovo record creato con successo";
  else   echo "Errore: " . $query . "<br>" . $mysqli_error($connessione);
}
$mysqli_close($connessione);
?>

Ecco chiaro il motivo della lunghezza della password a 128 caratteri: la funzione di hash scelta SHA512 ritorna una sequenza di caratteri di 128 elementi appunto. Esistono algoritmi che creano digest anche con più caratteri, ovviamente aumentando sicurezza ma anche complessità del calcolo. Potete trovare tutte le funzioni disponibili a standard PHP e la lunghezza del loro digest qui. Sconsiglio di usare MD5, che ancora esiste in PHP, poiché non ritenuta più infallibile e sicura anche se ancora largamente utilizzata per praticità e retro-compatibilità in molti sistemi. SHA1 ha anche verificato delle collisioni, così come SHA256 sembra essere troppo semplice da forzare, soprattutto se la password è molto semplice, come una parola di vocabolario, piuttosto che una sequenza di numeri/lettere. Nella vita pratica si aggiungono elementi di criptazione ulteriore in realtà per complicare la vita agli hacker che attaccano a forza bruta. Se un attaccante volesse provare ad indovina re una password potrebbe tranquillamente tentare 100 milioni di digest md5 in un solo secondo con le attuali CPU. Con SHA512 solo 10.000 prove al secondo. Se aggiungiamo la funzione BCRYPT o ARGON2, i tempi per indovinare si riducono drasticamente ma evitiamo di trattare l'argomento in questo articolo. Il lettore può approfondire qui. I server Wamp/Xamp hanno il solito problema di memoria stretta, la funzione sha512 è piuttosto ingorda e potrebbe dare problemi in virtù della sua complessità. Lo stesso esempio potete semplificarlo con md5 in caso.

A questo punto possiamo riscrivere lo script di login login.php. Il lettore potrà notare come siano state inserite anche le funzioni anti SQL Injection e come la password non venga usata in chiaro finalmente ma ne viene calcolato il digest e controverificato con la versione presente nel database dalla precedente registrazione. Se i digest sono uguali, lo saranno matematicamente anche le password che li hanno generati.

<?php
require_once("config.php");

/* Connessione e selezione del database */
$connessione = mysqli_connect($mysql_host,$mysql_user,$mysql_pass,$mysql_db);
if(mysqli_connect_errno())
  die("Connessione non riuscita: " . mysqli_error($connessione));

if (isset($_POST['password']) &&  isset($_POST['user']))
{
  $user = trim($_POST['user']);
  $user = stripslashes($user);
  $user = mysqli_real_escape_string($connessione,$user);
  $password = mysqli_real_escape_string($connessione, stripcslashes(trim($_POST['password'])));
  $passwordhash = hash("sha512", $password);
  $query = "SELECT * FROM users WHERE username = '$user' AND password = '$passwordhash'";

  $res = mysqli_query($connessione,$query) or die("Errore nella login: " . mysqli_error($connessione));

  if ($res)
      echo "Ciao utente loggato!";
  else
      echo "Errore: " . $query . "<br>" . $mysqli_error($connessione);

}
  $mysqli_close($connessione);
?>

 Gli script PHP e le pagine HTML sono volutamente scarne con poco codice. Al lettore personalizzare il codice aggiungendo elementi di grafica e CSS. Trovate il codice completo allegato.

Letto 227 volte
Prof. Alfredo Centinaro

Docente di "Scienze e tecnologie informatiche", "Tecnologie e progettazione di sistemi informatici", "Sistemi e Reti" presso IIS Alessandrini-Marino (Teramo), consulente e sviluppatore web. Ha collaborato per anni come sviluppatore presso MHT - Treviso, assistente Sistemi ed elaborazione dell'informazione in UniTE Corso di laurea in Scienze del turismo culturale, tutor presso Telecom Italia Learning Services (L'Aquila)

Joomla SEF URLs by Artio