OWASP ESAPI PHP: Autentificación y control de sesiones

En esta última entrega vamos a ver como gestionar la autentificación de usuarios y las sesiones. Empecemos por enumerar las características que vamos a implementar en nuestra clase User:

  • Mejorar la gestión de sesiones
  • Implementar tokens CSRF
  • Generar hashes de las claves
  • Leer propiedades de los objetos desde la base de datos
  • Limitar el tiempo de las sesiones (expire time)
  • Establecer qué guardamos en las variables de sesión



No es una lista exhaustiva pero nos va a permitir ver cómo usar ESAPI. ESAPI PHP dispone de algunos objetos y utilidades para gestionar usuarios aunque para la mayoría de requerimientos no son suficientes.   Entre las funciones más útiles y completas destaca la gestión de tokens CSRF mediante el control HTTPUtilities. Empecemos pues añadiendo a User.php las rutas de acceso a los controles ESAPI que necesitamos:

require("lib/conf.php");
require_once(ESAPI_SRC_path . "/ESAPI.php");
require_once(ESAPI_SRC_path . "/reference/BlogHTTPUtilities.php");

y le añadimos también nuevas propiedades:

private $esapi = null;
private $httputils = null;
private $logged_in = null;
private $salt = null;
private $expire_time = 600;

$esapi es nuestro objeto ESAPI, ya lo vimos anteriormente. Guardamos el control HTTPUtilities en $httputils. Como sólo hay un nivel de usuario usaremos una variable booleana $logged_in para guardar esta información. $salt la usaremos para implementar el hashing de las claves y $expire_time es el tiempo de inactividad antes de cerrar la sesión. El valor se $salt debe guardarse en la base de datos, así que vamos a modificar ésta haciendo el campo más grande:

ALTER TABLE user ADD salt VARCHAR(30);
alter table user change password password varchar(100) not null;

Veamos pues el constructor de nuestro User modificado:

function __construct($user_id='') {
     $this->esapi = new ESAPI(ESAPI_XML_path);
     ESAPI::setHTTPUtilities(new BlogHTTPUtilities());
     $this->httputils = ESAPI::getHTTPUtilities();
     $this->logged_in = false;
     if($this->check_user_session()) {
          if($this->retrieve_user()) {
               $this->logged_in = true;
          }
     }
}

Como ya vimos, inicializamos nuestros objetos ESAPI y establecemos el valor de $logged_in a false (no hay sesión abierta). A continuación comprobamos si hay una sesión activa y, de ser así, establecemos el valor de $logged_in a true. La función retrieve_user() la veremos después.

Creamos ahora unos sencillos métodos get/set para las propiedades:

function set_password($password) {
     $this->password = $password;
}

function get_password() {
     return $this->password;
}

function get_token() {
     return $this->httputils->getCSRFToken();
}

function get_logged_in() {
     return $this->logged_in;
}

function set_logged_in($logged_in) {
     $this->logged_in = $logged_in;
}

El único punto que no resulta evidente es la función get_token. El control HTTPUtilities se encarga de gestionar los tokens CSRF así que esta función sólo necesita llamar a HTTPUtilities.

Sigamos con la función de login:

function login($username, $password) {
     $db = DB::get_instance();

     $sql = $db->prepare("SELECT id, username, password, salt FROM user WHERE username = ?");
     $sql->bind_param('s', $username);
     if(!$sql->execute()) {
          $this->error_list[] = "Could not log in.";
          $sql->close();
          return false;
     }
     $sql->store_result();
     if($sql->num_rows() != 1) {
          $this->error_list[] = "Could not log in.";
          $sql->free_result();
          return false;
     }
     $sql->bind_result($user_id, $username, $stored_pass, $salt);
     $sql->fetch();
     $sql->free_result();
     $sql->close();

     if($this->hash_pass($password, $salt) != $stored_pass) {
          echo("<br>password = $password<br>salt = $salt<br>stored_pass = $stored_pass<br>hashed_pass = " . $this->hash_pass($password,$salt) . "<br>");
          $this->error_list[] = "Invalid password.";
          return false;
     }
     $this->user_id = $user_id;
     $this->username = $username;
     $this->password = $stored_pass;
     $this->salt = $salt;
     if(!$this->create_user_session()) {
          return false;
     }
     return true;
}

Lo primero que notamos es que, si se ha proporcionado un nombre de usuario válido, leemos toda su información en la base de datos. Esto se debe al uso del campo salt. Necesitamos hashear la clave que el usuario ha escrito para poderla comparar con la almacenada. También hemos cambiado ligeramente la comprobación de num_rows. Sabemos que la consulta no debe devolver más de una fila, así que lo verificamos explícitamente. A continuación hasheamos la clave suministrada y la comparamos con la de la base de datos. Si coinciden, creamos el usuario e iniciamos la sesión.

private function create_user_session() {
     session_start();
     if(!session_regenerate_id(true)) {
          $this->error_list[] = "Could not create user session. Please try again";
          return false;
     }
     $this->httputils->setCSRFToken();
     $_SESSION['user_id'] = $this->user_id;
     $_SESSION['expire_time'] = time() + $this->expire_time;
     return true;
}

En las versiones anteriores todo lo que hacíamos era inicializar user_id y username. Ahora, cuando un usuario se conecta, regeneramos cualquier id de sesión y generamos un token CSRF con el control httputils (que será pasado a cualquier request del usuario para prevenir ataques). No guardamos el nombre del usuario porque ya lo tenemos en la base de datos. Terminamos estableciendo el tiempo de expiración de la sesión.

La función check_user_session también ha ganado en complejidad:

function check_user_session() {
     session_start();
     $token = $_GET['token'];
     if(!$token) {
          $token = $_POST['token'];
     }
     if(!$token) {
          return false;
     }
     if(!$this->httputils->verifyCSRFToken($token)) {
          $this->error_list[] = "Could not verify session.";
          return false;
     }
     if(!$_SESSION['expire_time'] || time() > $_SESSION['expire_time']) {
          $this->expire_session();
          $this->error_list[] = "Session_expired";
          return false;
     }
     if(!$_SESSION['user_id']) {
          $this->error_list[] = "Session not found.";
          return false;
     } else {
          $this->user_id = $_SESSION['user_id'];
     }
     $this->update_expire_time();
     return true;
}

Básicamente hemos añadido más comprobaciones. Comprobamos el token CSRF mediante la función verifyCSRFToken del control HTTPUtilities. Esta es la función que modificaremos en nuestra librería modificada BlogHTTPUtilities (la función original depende de una funcionalidad que aún no se implementó al 100%). Seguidamente comprobamos el tiempo límite de la sesión y la cerramos si se ha superado y el user_id para terminar. Si todas las comprobaciones salen bien actualizamos el tiempo de la sesión y devolvemos true indicando que el usuario está conectado y la sesión es válida.

Las funciones para gestionar el límite de tiempo son:

function expire_session() {
     session_destroy();
}

function update_expire_time() {
     $_SESSION['expire_time'] = time() + $this->expire_time;
}

Veamos ahora la función retrieve_user que llamábamos desde el constructor:

private function retrieve_user() {
     if(!$this->user_id) {
          $this->error_list[] = "No user to retrieve!";
          return false;
     }
     $db = DB::get_instance();
     $sql = $db->prepare("SELECT username, password FROM user WHERE id = ?");
     $sql->bind_param('i', $this->user_id);
     if(!$sql->execute()) {
          $this->error_list[] = "Could not retrieve user.";
          $sql->close();
          return false;
     }
     $sql->store_result();
     if($sql->num_rows() != 1) {
          $this->error_list[] = "Could not retrieve user";
          $sql->free_result();
          $sql->close();
          return false;
     }
     $sql->bind_result($this->username, $this->password);
     $sql->fetch();
     $sql->free_result();
     $sql->close();
     return true;
}

Sencilla, igual a las que ya vimos en las anteriores unidades. Ahora necesitamos una manera de crear nuevos usuarios con su clave hasheada así que implementamos una función write:

function write() {
     $db = DB::get_instance();
     if(!$this->salt) {
          $this->gen_salt();
     }
     $hashed_pass = $this->hash_pass($this->password, $this->salt);
     $sql = $db->prepare("INSERT INTO user (username, password, salt) VALUES (?, ?, ?)");
     $sql->bind_param('sss', $this->username, $hashed_pass, $this->salt);
     if(!$sql->execute()) {
          $this->error_list[] = "Could not write user, please try again.";
          $sql->close();
          return false;
     }
}

Y para terminar, las funciones salt y hash para codificar nuestras claves:

private function gen_salt() {
     $this->salt = rand(1,100000) . time() . $this->username . rand(1,100000);
}

private function hash_pass($password, $salt) {
     return md5($password.$salt);
}

Modificamos ahora ligeramente la función verifyCSRFToken en BlogHTTPUtilites para gestionar los tokens:

public function verifyCSRFToken($token) {
     if(!$this->getCSRFToken() == $token) {
         throw new IntrusionException('Authentication failed.', 'Possibly forged HTTP request without proper CSRF token detected.');
         return false;
     }
     return true;
}

Y eso es todo. Hay otros cambios para la gestión de tokens y sesiones pero son muy simples y no se van a tratar aquí. Cuando se loggea un usuario debemos añadir su token CSRF a cualquier enlace y también comprobamos la propiedad logged_in en vez de acceder directamente a las variables.

El último punto a tratar es referente a post.php. En su versión original se asume que si un usuario entra en esta página es porque está loggeado. Evidentemente eso significa que cualquiera que acceda puede postear lo que quiera, por lo tanto debemos comprobar la sesión de usuario antes de postear. El codigo de esta última entrega puede descargarse aquí.

No hay comentarios:

Publicar un comentario