martedì 13 novembre 2012

Creare un login sicuro con PHP e MySQL pt. 2/4

Cari lettori, benvenuti nella seconda parte di questo articolo. Vi ricordo che potete trovare la prima parte del tutorial a questo indirizzo e, inoltre, ribadisco nuovamente il nostro intento: Il tema centrale del tutorial proposto non è tanto la grafica, che potrete ovviamente modificare e personalizzare a vostro piacimento, ma la sicurezza! Vedremo, infatti, i migliori metodi per criptare le password, per resistere ai brute force e per difenderci dalle SQL injection. Anche in questa seconda parte, a fine articolo troverete anche il codice sorgente pronto all'uso!

La pagina functions.php


Tutto il codice seguente farà parte di una pagina chiamata functions.php che conterrà, per l'appunto, tutte le funzioni che andremo poi ad includere e richiamare nelle altre pagine del nostro sito.

1. Inizializzare una sessione sicura in PHP
Prima di tutto, chiariamo uno dei concetti fondamentali sulla sicurezza delle sessioni. L'utilizzo della funzione predefinita "session_start ();" nella parte superiore di ogni pagina che si desidera rendere privata, non è sufficiente ad una sicurezza totale! Il metodo più efficace consiste, invece, nel creare una funzione chiamata "sec_session_start", che inizializzerà una sessione sicura in PHP. Anche in questo caso questa funzione andrà richiamata in ogni pagina che vorrete proteggere. Diamo un'occhiata al codice prima di procedere a commentarlo:

function sec_session_start() {
        $session_name = 'sec_session_id'; // Diamo un nome alla sessione
        $secure = false; // Impostate su true se utilizzate l'HTTPS
        $httponly = true; // Questa variabile impedisce al JS di accedere all'ID di sessione
 
        ini_set('session.use_only_cookies', 1); // Forza la sessione ad utilizzare unicamente i cookies
        $cookieParams = session_get_cookie_params(); // Preleva i parametri correnti dei cookies
        session_set_cookie_params($cookieParams["lifetime"], $cookieParams["path"], $cookieParams["domain"], $secure, $httponly); 
        session_name($session_name); // Setta il nome di sessione come dalla variabile
        session_start(); // Inizializzo la sessione
        session_regenerate_id(true); // Rigenero la sessione sovrascrivendo la precedente
}
Questa funzione rende impossibile sovrascrivere l'ID della sessione tramite javascript (ad esempio nel caso di attacco XSS). Infatti la "session_regenerate_id ()" è una funzione che rigenera l'id di sessione ad ogni reload della pagina, aiutando a prevenire il dirottamento di sessione.

N.B. Se utilizzi questo codice su una pagina protetta da HTTPS la variabile '$secure' va settata su true.

2. Il login sicuro

È giunto finalmente il momento di vedere il codice della funzione che si occupa del login vero e proprio.
function login($email, $password, $mysqli)
{
    // Utilizare dichiarazioni pre-formate ci protegge dalle SQL injection 
    if ($stmt = $mysqli->prepare("SELECT id, username, password, salt FROM members WHERE email = ? LIMIT 1")) {
        $stmt->bind_param('s', $email); // Leghiamo il valore dell'email al parametro
        $stmt->execute(); // Eseguiamo la query
        $stmt->store_result();
        $stmt->bind_result($user_id, $username, $db_password, $salt); // preleviamo il risultato
        $stmt->fetch();
        $password = hash('sha512', $password . $salt); // codifichiamo la password con un codice unico
        
        if ($stmt->num_rows == 1) { // Se l'utente esiste
            // Controlliamo che l'utente non abbia effettuato troppi tentativi di accesso
            if (checkbrute($user_id, $mysqli) == true) {
                // Se l'account è bloccato
                // Inviamo un email all'account per avvertirlo
                return false;
            } else {
                if ($db_password == $password) { // Controlliamo la password
                    // Se la password è corretta
                    
                    $ip_address               = $_SERVER['REMOTE_ADDR']; // Preleviamo l'indirizzo IP dell'utente
                    $user_browser             = $_SERVER['HTTP_USER_AGENT']; // Risaliamo al browser utilizzato dall'utente
                    // Seguono i codici per la protezione XSS
                    $user_id                  = preg_replace("/[^0-9]+/", "", $user_id);
                    $_SESSION['user_id']      = $user_id;
                    $username                 = preg_replace("/[^a-zA-Z0-9_\-]+/", "", $username);
                    $_SESSION['username']     = $username;
                    $_SESSION['login_string'] = hash('sha512', $password . $ip_address . $user_browser);
                    // Login effettuato con successo
                    return true;
                } else {
                    // Se la password non è corretta
                    // Registriamo i dati del tentativo nella sessione
                    $now = time();
                    $mysqli->query("INSERT INTO login_attempts (user_id, time) VALUES ('$user_id', '$now')");
                    return false;
                }
            }
        } else {
            // Non esistono utenti
            return false;
        }
    }
}
3. Controllo dei tentativi di brute force

Il brute force, come forse saprete, è una tecnica nella quale il malintenzionato che vuole accedere all'area riservata prova migliaia e migliaia di password generate casualmente o prelevate da un dizionario attraverso un programma automatico. La nostra funzione farà si che, dopo 5 tentativi errati di login, l'utente in questione risulterà bloccato. Vediamo come:
function checkbrute($user_id, $mysqli) {
   // Preleviamo l'orario corrente
   $now = time();
   // Tutti i tentativi di login vengono immagazzinati per 2 ore dal primo tentativo 
   $valid_attempts = $now - (2 * 60 * 60); 
 
   if ($stmt = $mysqli->prepare("SELECT time FROM login_attempts WHERE user_id = ? AND time > '$valid_attempts'")) { 
      $stmt->bind_param('i', $user_id); 
      // Eseguiamo la query
      $stmt->execute();
      $stmt->store_result();
      // Se ci sono stati più di 5 tentativi
      if($stmt->num_rows > 5) {
         return true;
      } else {
         return false;
      }
   }
}
Vi ricordo che altri modi per impedire l'uso del brute force sono: inserire un test CAPTCHA oppure ritardare temporaneamente la possibilità di accedere ad ogni tentativo di login fallito.

4. Controllo dello stato di accesso

Il controllo dello stato di accesso avviene selezionando le variabili di sessione "user_id" e "login_string". La variabile "login_string" ha immagazzinato al suo interno gli IP degli utenti, le informazioni del browser e la password criptata.
function login_check($mysqli) {
   // Controllo che tutte le variabili siano state settate
   if(isset($_SESSION['user_id'], $_SESSION['username'], $_SESSION['login_string'])) {
     $user_id = $_SESSION['user_id'];
     $login_string = $_SESSION['login_string'];
     $username = $_SESSION['username'];
     $ip_address = $_SERVER['REMOTE_ADDR']; // Prelevo l'IP dell'utente. 
     $user_browser = $_SERVER['HTTP_USER_AGENT']; // Prelevo i dati del browser
 
     if ($stmt = $mysqli->prepare("SELECT password FROM members WHERE id = ? LIMIT 1")) { 
        $stmt->bind_param('i', $user_id); // Leghiamo "$user_id" al parametro
        $stmt->execute(); // Eseguiamo la query
        $stmt->store_result();
 
        if($stmt->num_rows == 1) { // Se l'utente esiste
           $stmt->bind_result($password); // Preleviamo la variabile dal risultato
           $stmt->fetch();
           $login_check = hash('sha512', $password.$ip_address.$user_browser);
           if($login_check == $login_string) {
              // Login effettuato con successo
              return true;
           } else {
              // Altrimenti: non loggato
              return false;
           }
        } else {
            // Altrimenti: non loggato
            return false;
        }
     } else {
        // Altrimenti: non loggato
        return false;
     }
   } else {
     // Altrimenti: non loggato
     return false;
   }
}

CONCLUSIONI E SORGENTI

Così si conclude la seconda delle quattro parti di questo tutorial. Per qualsiasi incomprensione o dubbio potete commentare questo articolo o inviarmi un email a noframe@lucapipolo.it. Potete anche scaricare tutto il codice sorgente da questo link. Alla prossima!

0 commenti:

Posta un commento