¡Hola! Me parece que andas usando un bloqueador de anuncios =( ¿Nos desbloqueas? ¿Por qué?
F13

Funcion 13

Prevén la falsificación de petición en sitios cruzados (CSRF)

La falsificación de petición en sitios cruzados (CSRF por sus siglas en inglés) es una vulnerabilidad bastante común en la que se consigue que un usuario lleve acabo una acción que no pretendía.

Esto puede ocurrir cuando, por ejemplo, el usuario ha iniciado sesión en una de sus webs y hace click sobre un enlace aparentemente inofensivo. Por detrás, la información de su perfil es actualizada y se cambia su correo electrónico con el de un atacante. El atacante ahora puede solicitar un reseteo de contraseña sin que nadie se entere y ha robado la cuenta con éxito.

He visto muchos sitios, empresas incluso, que no han oído hablar de CSRF y no tienen mucha idea de cómo funciona. Si te encuentras entre aquellos que no tienen ni idea, te recomiendo encarecidamente que sigas leyendo para que aprendas a evitar esta vulnerabilidad. En esta pequeña guía vamos a ver cómo funciona y cómo podemos prevenir que ocurra.

¿Cómo funciona un ataque CSRF?

Para entender bien cómo funciona un ataque CSRF,  mejor (como dice el gran Goyo Jiménez) no lo cuento, lo hago. Para ilustrar un ataque, vamos a crear un sencillo ejemplo que te permite cerrar una sesión activa. Pero antes de cerrar una sesión activa, vamos a necesitar una página de login (login.php), un script que se encargue de iniciar y cerrar la sesión (procesar.php) y finalmente nuestro ataque de ejemplo (gatito.html). Los gatitos son inocentes, ¿verdad?

Veamos el código de login.php

<?php  
session_start();  
?>
<html>  
 <body>
<?php  
if (isset($_SESSION["usuario"])) {  
    echo "<p>¡Bienvenido, " . $_SESSION["usuario"] . "!<br>";
    echo '<a href="procesar.php?accion=logout">Salir</a></p>';
}
else {  
?>
  <form action="procesar.php?accion=login" method="post">
   <p>El usuario es: admin</p>
   <input type="text" name="usuario" size="20">
   <p>Contraseña: prueba</p>
   <input type="password" name="pass" size="20">
   <input type="submit" value="Entrar">
  </form>
<?php  
}
?>
 </body>
</html>  

El script inicializa primero los datos de sesión. Luego revisa si la variable $_SESSION["usuario"] está establecida. De ser así, nos muestra un mensaje de bienvenida y un enlace para cerrar la sesión. Si no, muestra el formulario de inicio de sesión.

Vamos ahora con el procesado en procesar.php:

session_start();  
switch ($_GET["accion"]) {  
    case "login": // Si viene por POST
        if ($_SERVER["REQUEST_METHOD"] == "POST") {
            $usuario = (isset($_POST["usuario"]) && $_POST["usuario"]) ? $_POST["usuario"] : null;
            $pass = (isset($_POST["pass"])) ? $_POST["pass"] : null;
            $salt = '#l0S.p4Nd4s.Pr0T3g3N.3sT4.cL4v3#';
            if (isset($usuario, $pass) && (crypt($usuario . $pass, $salt) == crypt("adminprueba", $salt))) {
                $_SESSION["usuario"] = $_POST["usuario"];
            }
        }
    break;
    case "logout":
        $_SESSION = array();
        session_destroy();
    break;
}
header("Location: login.php");  

Este script comienza también iniciando los datos de sesión, y luego revisa si hay alguna acción sobre la que trabajar. Revisamos que los datos vengan rellenos con operadores ternarios y hacemos una sencilla comprobación con la función crypt(). Si esto fuera un caso real, lo comprobaríamos seguramente con los datos que tengamos almacenados en base de datos. Finalmente, el usuario es redirigido a la página login.php al final del script.

Finalmente, lleguemos a la parte de los gatitos:

<p>Ohh mira un gatito... ¡Qué bonito!</p>  
<img src="procesar.php?accion=logout" style="display: none;">  

Si visitas la página login.php e inicias sesión, y mientras has iniciado sesión visitas esa página, quedarás automáticamente deslogado incluso sin que hayas hecho nada. El navegador envía una petición al servidor para acceder al script de procesar.php, esperando que sea una imagen.

Nuestro script inicial no tiene forma de diferenciar entre una petición válida, iniciada por el click de un usuario sobre el enlace de “Salir”, y una petición diseñada para hacer el mal.

Lo peor, es que el archivo gatito.html puede estar en un servidor completamente diferente al de tu aplicación y funcionar correctamente porque la página atacante hace una petición en tu nombre usando la sesión que abriste antes. Incluso si la web está en una red interna funcionará porque la petición se envía desde tu IP como si tú hubieras hecho la petición por tu cuenta.

Además, si permites a tus usuarios enlazar a imágenes como avatares de perfil sin escapar apropiadamente, tendrás el enemigo en casa.

Vale vale… cerrar la sesión de alguien no es muy grave. Pero bien podríamos aprovechar cualquier acción a nuestro alcance, o incluso un formulario oculto que se autoenvíe cuando se cargue la página.

¿Ves ahora la seriedad de un ataque CSRF? Veamos pues cómo podemos resolverlo.

Protege a tus usuarios

Para poder asegurarte de que una acción la está llevando acabo un usuario en vez de un script malicioso, tenemos que asociar un identificador único para que podamos verificarlo en cada acción.

Vamos a modificar el archivo login.php para que prevenir el ataque:

<?php session_start();  
if (isset($_SESSION["usuario"])) { // Generando ID único, podríamos usar la id de sesión  
    $_SESSION["token"] = md5(uniqid(mt_rand(), true));
    echo "<p>¡Bienvenido, " . $_SESSION["usuario"] . "!<br>";
    echo '<a href="procesar.php?accion=logout&token=' . $_SESSION["token"] . '">Salir</a></p>';
} else { ?> 
  <form action="procesar.php?accion=login" method="post"> 
    <p>El usuario es: admin</p> 
    <input type="text" name="usuario" size="20"> 
    <p>Contraseña: prueba</p> 
    <input type="password" name="pass" size="20"> 
    <input type="submit" value="Entrar"> 
    <input type="hidden" value="<?php echo $_SESSION['token']; ?>"> </form> 
    <?php
} ?> 

Luego, para verificarlo en procesar.php:

session_start();  
$token = isset($_GET["token"]) ? $_GET["token"] : (isset($_POST["token"]) ? $_POST["token"] : null);
if ($token == $_SESSION["token"]) {  
    switch ($_GET["accion"]) {
        case "login": // Si viene por POST
            if ($_SERVER["REQUEST_METHOD"] == "POST") {
                $usuario = (isset($_POST["usuario"]) && $_POST["usuario"]) ? $_POST["usuario"] : null;
                $pass = (isset($_POST["pass"])) ? $_POST["pass"] : null;
                $salt = '#l0S.p4Nd4s.Pr0T3g3N.3sT4.cL4v3#';
                if (isset($usuario, $pass) && (crypt($usuario . $pass, $salt) == crypt("adminprueba", $salt))) {
                    $_SESSION["usuario"] = $_POST["usuario"];
                }
            }
        break;
        case "logout":
            $_SESSION = array();
            session_destroy();
        break;
    }
}
header("Location: login.php"); ?>  

Con estas sencillas modificaciones, gatito.html ya no funcionará porque el atacante tendría que adivinar un token adicional que es aleatorio. Como véis, hemos protegido también el formulario para que el token se envíe junto al resto de datos.

Cabe destacar, que esta medida puede ser aplicada de forma similar a las peticiones AJAX.

Como rápido ejemplo, si accedéis a cualquier servidor con phpMyAdmin y lo usáis, veréis que en todas las peticiones tiene un token en la URL:

Si el artículo te pareció interesante, útil o incluso equivocado, por favor considera el dejar un comentario. ¡Lo apreciaré mucho!

Programador Front-end en First + Third y Potato. Trabajando con JavaScript y HTML5 desde el corazón de Sevilla.

Comentarios ¡Únete a la conversación!