Logo
You
Code

CSRF con Tokens en PHP

Autor Patxi Echarte - http://www.youcode.com.ar/php/csrf-con-tokens-en-php-111

Vamos a explicar cómo proteger nuestras aplicaciones PHP contra ataques de tipo CSRF. Este tipo de ataques hacen que el usuario realice acciones sobre un web de forma inadvertida. Por ejemplo, en una página vulnerable a este tipo de ataques, podrían construirse páginas de ataque en las que únicamente con la visita del usuario se modificase su información de registro, se publicase información de forma oculta, etc.

Token personal por accion en session

Esta forma de generación de tokens se basa en la creación de tokens específicos para cada tipo de acción realizable en la aplicación y el registro de estos tokens en la sesión del usuario. La siguiente función recibe como parámetro el nombre de un tipo de formulario o de acción realizable sobre el web. La función genera un token de forma aleatoria y guarda en sesión, asociado al nombre del formulario recibido, el token generado y la fecha de generación.
function generateFormToken($form) {
 
   // generar token de forma aleatoria
   $token = md5(uniqid(microtime(), true));
 
   // generar fecha de generación del token
   $token_time = time();
 
   // escribir la información del token en sesión para poder
   // comprobar su validez cuando se reciba un token desde un formulario
   $_SESSION['csrf'][$form.'_token'] = array('token'=>$token, 'time'=>$token_time);; 
 
   return $token;
}

Para incluir el token en un formulario habría que añadir un nuevo campo oculto a cada formulario que queramos securizar. En el siguiente ejemplo se ha añadido el campo “auth_token” a un formulario de envío de mensajes. En el campo “value” del campo se incluye el token obtenido indicando que se solicita un token para el formulario “send_message”.
 
<form action="send_message.php" method="POST">
<input type="hidden" name="auth_token" value="<?=generateFormToken('send_message')?>" />
<textarea name="message-text"></textarea>
<input type="submit" />
</form>

La siguiente función permite comprobar la validez de los tickets generados. La función recibe como parámetros el nombre del formulario, el token recibido y un parámetro opcional con el tiempo de validez del ticket. Se comprueba si hay registrado en sesión un token de autorización para el formulario indicado y, si es así, se compara este token con el recibido. En caso de que sean iguales se tiene en cuenta si se indica un tiempo de validez de ticket. Este tiempo permite establecer un control complementario de forma que damos un tiempo de vida determinado a cada ticket enviado al usuario.
 
function verifyFormToken($form, $token, $delta_time=0) {
 
   // comprueba si hay un token registrado en sesión para el formulario
   if(!isset($_SESSION['csrf'][$form.'_token'])) {
       return false;
   }
 
   // compara el token recibido con el registrado en sesión
   if ($_SESSION['csrf'][$form.'_token']['token'] !== $token) {
       return false;
   }
 
   // si se indica un tiempo máximo de validez del ticket se compara la
   // fecha actual con la de generación del ticket
   if($delta_time > 0){
       $token_age = time() - $_SESSION['csrf'][$form.'_token']['time'];
       if($token_age >= $delta_time){
      return false;
       }
   }
 
   return true;
}

Para comprobar la validez del ticket cuando se recibe un formulario de envío de mensajes por POST, podemos utilizar el siguiente código, en el que se llama a la función anterior indicando que se quiere comprobar la validez del token recibido para el formulario “send_message” con un tiempo máximo de vida de 300 segundos (5 minutos).
 
$ticket = $_POST['auth_token'];
$valid = verifyFormToken('send_message', $token, 300);
if(!$valid){
   echo "El ticket recibido no es válido";
}

Con esta forma de generar los token securizamos cada formulario de la aplicación por separado. En el caso de que alguno de los tickets generados cayerá en poder de un atacante únicamente podría utilizarlo en un formulario y durante un tiempo de vida limitado. En el lado negativo hay que tener en cuenta el espacio requerido en el servidor para almacenar cada token en las sesiones de usuario y algunos problemas de usabilidad. Estos problemas pueden producirse por ejemplo si el usuario utiliza varias pestañas para navegar por el web. En este caso, podría darse el caso de tener dos pestañas abiertas con el formulario de enviar mensaje, pero cada una de ellas con un ticket diferente. Al enviar un mensaje únicamente el último ticket generado sería válido. Una forma de reducir este problema sería comprobando también la fecha de validez del ticket en la generación y no regenerándolo en cada petición de ticket.

Token personal por accion sin session

Esta forma de generar los tickets se basan en la utilización de una palabra secreta y el identificador de sesión del usuario. De esta forma se evita la necesidad de tener que almacenar los tokens generados en sesión. A continuación se muestran dos funciones que pueden utilizarse en lugar de las descritas en la sección anterior. Ambas funciones utilizan un valor de configuración prefijado que contiene una palabra secreta a partir de la cual se generan todos los tickets. Esto permite invalidar en cualquier momento todos los tickets modificando simplemente esta palabra.
$CONF['csrf_secret'] = 'dfa%d_FA{]2Ñf523scvDAgfasg';
 
public function generateFormToken($form) {
   $secret = $GLOBALS['CONF']['csrf_secret'];
   $sid = session_id();
   $token = md5($secret.$sid.$form);
   return $token;
}
 
public function verifyFormToken($form, $token) {
   $secret = $GLOBALS['CONF']['csrf_secret'];
   $sid = session_id();
   $correct = md5($secret.$sid.$form);
   return ($token == $correct);
}

La función de generación recibe el nombre del formulario a securizar y obtiene el token asociado haciendo un hash md5 sobre la concatenación de la palabra secreta, el identificador de sesión del usuario y el nombre del formulario. De esta forma se obtiene un token de autorización para cada usuario y cada acción. Para comprobar su validez se realiza la misma acción y se compara el token generado con el token recibido.

A diferencia del caso en el que registrabamos los token en sesión utilizamos menos espacio en el servidor y tenemos menos problemas de usabilidad, pero sin embargo no tenemos control sobre el tiempo de validez del ticket, estos caducan en el momento en el que finaliza la sesión de usuario. El siguiente fragmento de código permite comprobar la validez de un token recibido.

$ticket = $_POST['auth_token'];
$valid = verifyFormToken('send_message', $token);
if(!$valid){
   echo "El ticket recibido no es válido";
}
http://www.youcode.com.ar/php/csrf-con-tokens-en-php-111