lunes, 22 de julio de 2013

Seguridad PHP - Cross Site Request Forgery (CSRF)

Los ataques XSS se centraban en explotar la confianza de un usuario en un sitio web, ya que confiaba en la seguridad del servidor para que los atacantes no pudieran apoderarse de información de los usuarios, mediante la inserción de código 'malicioso'.
Pues existen otro tipo de ataques cuya idea es parecida. Este tipo de ataques ocurren cuando un sitio web permite que un usuario autenticado realice acciones sin verificar si realmente es él quien las está realizando. El ataque CSRF puede hacer que el navegador de la víctima envíe una petición HTTP 'fraudulenta'.  Y la aplicación (servidor) vulnerable piensa que es una petición de la víctima. Esto permitiría al atacante forzar al navegador de la víctima a hacer peticiones que la aplicación vulnerable pensaría que son peticiones legítimas.

Ejemplos

- Ataque mediante imágenes

En los foros o redes sociales es común que se permita subir imágenes  por parte del usuario, junto a los mensajes. El atacante puede ocultar en ella un link. Este link podrá contener acciones (parámetros )que serán enviada a través de la URL. Las acciones pueden implicar el cambio de contraseñas o envío de información de usuario.
Estos enlaces se pueden ocultar ya que este tipo de  páginas pueden permitir modificar el código HTML del mensaje a enviar. Y Suponiendo que www.foroejemplo.com es el foro actual donde se cometerá el ataque, por ejemplo podríamos insertar algo así:

 <a href="www.foroejemplo.com/action=logout" target="blank"><img src="..."/></a>  

Y poner una descripción atractiva para que algún usuario de la aplicación haga click en la imagen.

Pero también se podría prescindir del enlace:

 <img src="www.foroejemplo.com/action=logout" alt="p" height="1" width="1"/>  

Con lo que la imagen fallará en cargar pero ejecutará la url.

Pero es que siguiendo esta técnica, el atacante podría hacer que un usuario haga una petición, sin saberlo, a una página fraudulenta con la única idea de monitorizar y apropiarse de la cookie de sesión. Ya que el usuario está 'logeado' en el foro y por lo tanto por las cabeceras viajan también las cookies de identificación del foro. Y con esto intentar otros ataques como la fijación de sesión.

- Aprovechamiento del uso excesivo peticiones GET

Hay sitios web que permiten realizar acciones críticas mediante peticiones GET, lo cual facilita la gravedad de los ataques CSRF. Imagina este ataque anterior de la imagen:

 <img style="display:none;" src="http://objetivo.com/change_password.php?new_password=1234">  

Un atacante puede engañar a un usuario identificado en el sitio víctima, y con sesión activa, a que entre en un sitio web malicioso. Esto no es tan raro de ver, Por ejemplo es posible en correo spam que sugiere seguir enlace para sorteo. El sitio malicioso puede realizar peticiones al sitio víctima sin el permiso del usuario.  Estas se pueden hacer sin que se entere el usuario ya que mediante JavaScript podemos crear y enviar formularios (página con auto-envío de formulario). Al enviar esa petición fraudulenta también se estarán enviando las cookies de sesión del usuario. Y como serán correctas para el sitio victima, la petición se llevará a cabo.

- Ataque mediante peticiones POST

Aunque todas las peticiones de un sitio se realizan mediante POST, esto no asegura que el sitio este portegido contra CSRF. Ya que mediante JavaScript un atacante puede crear y enviar formularios:

 document.write('<form action="" method="post"> <input name="name" value="hola"><input name="birth_day" value="28"></form>');  

Por lo que estamos en el mismo caso que con el ejemplo anterior. Y si se consigue engañar a la victima para apoderarse de su identificador de sesión (cookie) se podrán enviar peticiones fraudulentas en su nombre.
Además el atacante podría usar iframes para ocultar la respuesta del servidor sobre la petición mediante el formulario.

- Ataque mediante iframe

Podemos enviar mediante JavaScript, como en los ejemplos anteriores, un formulario con campos ocultos. Y si el target (lugar donde se mostrará la respuesta del formulario) del formulario es un iframe oculto, la respuesta permanecerá oculta para el usuario.

 <iframe style="width: 0px; height: 0px; visibility: hidden" name="hidden"></iframe>  
 <form name="csrf" action="http://example.org/account/edit" method="post" target="hidden">  
      <input type="hidden" name="email" value="attacker@email.tld" />  
 </form>  
 <script>document.csrf.submit();</script>  

Prevención


A) Peticiones GET

Para una mejor protección es necesario que haya un buen diseño de la aplicación o sitio web. Para ello no debemos permitir la modificación de recursos mediante peticiones GET.
Esto nos protegería del ataque CSRF mediante el uso del tag o otros mediante peticiones GET. Ya que en el src de la imagen solo podemos especificar una url y query strings. Y esto viajará por la URL sin efecto. Ya que el servidor esperará peticiones POST.

B) Formularios

Como podemos ver, un punto conflictivo son los formularios. Ya que si no hay ningún mecanismo para asegurar que el usuario legítimo es quien está enviando dichos formularios, un atacante podría estar usando las “credenciales” de un usuario y hacerse pasar por él.

La idea sería que el servidor envíe un token diferente junto con la respuesta a cada petición a un formulario. Y que al recibir la petición del cliente pueda comprobar si se ha modificado o no. De esta manera podría asegurarse que el formulario ha sido enviado por el usuario legítimo identificado.

Aquí tendríamos una primera versión de como generar el token y guardarlo en la sesión del usuario que ha realizado la petición de mostrar el formulario. El servidor devolvería el token junto con el formulario. Y así, cuando el usuario envíe los datos del formulario comprobaremos que los tokens (el de la sesión y el del formulario) son iguales. Y por lo tanto no ha cambiado y pertenece al mismo usuario que hizo la primera petición:

 $token = md5(uniqid(microtime(), true));  
 $token_creation = time()  
 $_SESSION['csrf']= array('token'=>$token, 'token_creation'=>$token_creation);  

Además podemos (es aconsejable) controlar el tiempo que queremos que el token sea válido. Guardando para ello una marca de tiempo.

Pero vamos a mejorar un poco la creación del token: La función de PHP uniqId() no es del todo aconsejable para crear tokens criptográficos ya que puede crear identificadores predecibles.
Por lo que vamos a generar uno con la siguiente función  openssl_random_pseudo_bytes(). Le indicaremos como parámetros el número de bytes de la cadena. Además codificaremos el resultado de la anterior función  en base 64 para que no de problemas con el HTML.

 $token = base64_encode( openssl_random_pseudo_bytes(32));  

Ahora podemos comprobar que el  token enviado por el formulario es el correcto. Además sería conveniente almacenar un identificador del formulario al que pertenece el token. Ya que es normal que una página tenga varios formularios y así evitaremos problemas con las sesiones.

Vamos a ver un ejemplo completo repartido en 3 ficheros:

funciones.php
 <?php  
 define('TOKEN_DURATION', 200); //debe de definirse en fichero de configuración  
 function creaToken($idPagina) {  
   $token = base64_encode(openssl_random_pseudo_bytes(32));  
   $token_creation = time();  
   $_SESSION['csrf'][$idPagina] = array('token' => $token, 'token_creation' => $token_creation);  
 }  
 function compruebaTokenValido($idPagina, $tokenRecibido) {  
   if (!isset($_SESSION['csrf'][$idPagina])) {  
     return false;  
   }  
   if ($_SESSION['csrf'][$idPagina]['token'] !== $tokenRecibido) {  
     return false;  
   }  
   if (time() - $_SESSION['csrf'][$idPagina]['token_creation'] > TOKEN_DURATION) {  
     return false;  
   }  
   return true;  
 }  
 ?>  
     </form>  
   </body>  
 </html>  

cambioPass.php
 <?php  
 session_start();  
 include './functions.php';  
 $idPagina = 'cambio_pass';  
 if (isset($_POST['auth_token'])) {  
   $tokenRecibido = $_POST['auth_token'];  
   if (compruebaTokenValido($idPagina, $tokenRecibido)) {  
     //acción con el datos del formulario  
     //...  
     header("Location: ");  
     exit();  
   }  
 }  
 //si no se recibe datos del formulario o falla la comprobacion de token  
 //mostraremos formulario HTML  
 header("Location: ./cambioPass_form.php");  
 ?>  

cambioPass_form.php
 <?php  
 session_start();  
 include './functions.php';  
 $idPagina = 'cambio_pass';  
 creaToken ($idPagina);  
 $tokenActual = $_SESSION['csrf'][$idPagina]['token'];  
 ?>  
 <html  
   <head></head>  
   <body>  
     <form action="cambioPass.php" method="POST">  
       <!--componentes del formulario-->  
       <input type="submit" value="enviar"/>  
       <input type="hidden" name="auth_token" value="<?php echo $tokenActual ?>"/>  
     </form>  
   </body>  
 </html>  

Vemos como el token lo insertaremos en campo oculto del formulario HTML. Evidentemente faltaría mostrar errores cuando se redirige nuevamente al formulario en caso de que el token no concuerde. Sin en uso de plantillas, que veremos en otros tutoriales, podríamos guardar los errores en sesiones (en cambioPass.php) y acceder a ellos en el script que muestra el formulario (cambioPass_form.php).


Entradas relacionadas

Formularios y PHP

No hay comentarios:

Publicar un comentario