viernes, 26 de marzo de 2010

Seguridad en PHP: CSRF o Falsificación de petición en sitios cruzados

De entre todos los ataques que puede recibir tu sistema en PHP los CSRF o (Cross-site request forgery) son los más fáciles de implementar, ya que éstos se aprovechan de la "confianza" que le tiene el sistema al usuario.

¿pero como que mi sistema confía en el usuario?: pues es simple, si lo miras desde el punto de vista del servidor, cuando éste recibe un formulario, simplemente recibe los datos del formulario, porque se supone que el usuario ya está logueado (o ha iniciado sesión, cosa que procesas previamente obvio) y por ende él solo se encarga de una cosa: procesar la data y generar la salida.

Supongamos que nuestro querido usuario está ingresando registros nuevos en un CRUD de nuestro sistema, y por cosas del destino se dispone a chatear en messeger o hasta en el mismo facebook, como estos sitios te permiten compartir links, un atacante puede simplemente pasarle un link a nuestro usuario para que ingrese en determinada página, cuya página esta preparada para hacer un submit de un formulario oculto hacia el url de nuestro servidor, pudiendo enviar data hacia el archivo php que se encarga de "crear" ese nuevo registro, como el usuario está logueado y generalmente el navegador mantiene la cookie de la sesión (depende del navegador), el ataque maestro logra pasar el sistema de login y pudiendo afectar a nuestro sistema; en ningún caso el código de nuestro php espera una identificación del formulario, porque simplemente validamos las sesiones; otra modalidad consisten en colocar links de peticiones hacia el mismo domino en imágenes, por ejemplo (<img src="http://mydomain/archivo.php?dato=valor" alt="" />) en cuyo caso del lado del servidor (php por ejemplo) no verifique el tipo de petición Post o Get, ¿donde se evidencia?: en el mal uso de $_REQUEST en el caso de PHP.

¡¡¡OMG ¿y como lo resuelvo?!!!: pues por suerte la solución es tan simple que da risa, este pequeño (y menudo) inconveniente se resuelve con los llamados tokens; los tokens son simplemente un ID único que podremos generar al momento que nuestro usuario solicita el formulario, de esta forma al crear nuestro formulario añadimos un dato oculto adicional que nos permitirá verificar (cuando el formulario sea recibido) que dicha data es confiable, para ello ese mismo ID necesitamos registrarlo en el servidor y nada mejor que con una variable de sesión, y no sólo eso para darle más gusto a la sopa es preferible marcar el tiempo justo en que el form se creó, para así verificar cuanto tiempo se demoró (depende de la cantidad de data en el formulario) y hacerlo más seguro, es inverosímil que un formulario tan simple se demore tanto en ser enviado, así sancionamos además a los usuarios perezosos XD.

Pero, si tengo que crear un ID para cada formulario, ¿necesito abrir todos los php relacionados y añadírselos manualmente, ¡menuda tarea!?: sí, es lamentable, pero es la única forma, aunque gracias a la re-utilización de código la tarea es más fácil, simplemente creamos una clase que automatice esta tarea y así ahorrarnos muchas lineas de código en cada formulario que tengamos:


archivo tokenform.php: (PHP 5.0+)
<?php
/**
* administra un ID único (tokens) para un formulario previamente registrado
* GPL versión 1
* @author Maycol Alvarez
*/
class tokenForm {

/**
* Constante para prefijos de la las variables de sesion
*/
const _prefix = 'token_';

/**
* Nombre del formulario
* @var string
*/
private $_formname;
/**
* Tiempo en segundos para validar caducidad del token
* @var int
*/
private $_seconds=300;

/**
* Construye un manejador de tokens
* @param string $nameForm Nombre del formulario
* @param bool $preventSessionStart previene el inicio de sesion omitido
*/
public function __construct($nameForm,$preventSessionStart=true) {
if($preventSessionStart){
@session_start();
}
$this->_formname=self::_prefix.$nameForm;
//genera el token si no existe
if(!isset($_SESSION[$this->_formname])){
$this->create();
}
}

/**
* Crea los datos de sesion del token
*/
public function create(){
$token= md5(uniqid(rand(), true));
$timestamp = mktime();
$_SESSION[$this->_formname]['uid']=$token;
$_SESSION[$this->_formname]['mkt']=$timestamp;
}

/**
* Devuelve el UID del token
* @return string token
*/
public function getToken(){
return $_SESSION[$this->_formname]['uid'];
}

/**
* Devueve la fecha de creacion del token
* @return int mktime
*/
public function getTime(){
return $_SESSION[$this->_formname]['mkt'];;
}

/**
* Valida si el token del formulario enviado es confiable
* @param mixed $source permite cambiar el origen del array con la data del
* formulario, por defecto es null y se utiliza $_POST
* @return bool true en caso de exito
*/
public function validate($source=null) {
if($source==null){
$srctoken=$_POST[$this->_formname];
}else{
$srctoken=$source[$this->_formname];
}
//valida si los tokens coinciden:
if($this->getToken()==$srctoken){
//valida si se evalua caducidad:
if($this->_seconds!=0){
//ahora valida si estan el el rango de tiempo
if((mktime() - $this->getTime())<= $this->_seconds){
$returned= true;
}else{
$returned= false;
}
}else{
$returned= true;
}
}else{
$returned= false;
}
$this->clear();
return $returned;
}

/**
* Devuelve el input correspondiente al token para el form
* @return string Input HTML tag
*/
public function getInput(){
return sprintf(
'<input type="hidden" name="%s" value="%s" />',
$this->_formname,
$this->getToken()
);
}

/**
* Imprime el input correspondiente al token para el form
*/
public function dump() {
echo $this->getInput();
}

/**
* Borra el token
*/
public function clear(){
unset($_SESSION[$this->_formname]);
}

/**
* Establece el margen de segundos para considerar caduco un token
* @param int $seconds 0 para omitir esta validacion
*/
public function setTime($seconds){
$this->_seconds=$seconds;
}
}
?>


Con esta simple clase resolvemos muchas de las funciones que debe cumplir un token, para ejemplo veamos su funcionamiento en el archivo que crea el formulario, y el que lo procesa:


archivo formulario.php:
<?php
require_once 'tokenform.php';
$formulario = new tokenForm('form1');
?>
<form action="proceso.php" method="POST">
<input type="text" name="dato1" />
<input type="text" name="dato_n" />
<?php $formulario->dump(); ?>
<input type="submit" />
</form>


archivo proceso.php:
<?php
//procesando el token
require_once 'tokenform.php';
$formulario = new tokenForm('form1');
if(!$formulario->validate()){
echo 'Formulario no valido';
}
?>


Y con tan pocas líneas por archivo ya resolvemos semejante problema y hacemos a nuestro sistema más seguro XD.