Qué es un ataque CSRF y cómo prevenirlo

Introducción

Hoy vamos a ver qué es un ataque CSRF. Es un ataque que ya es viejo pero sin dudas muchos de nosotros seguimos teniendo esa vulnerabilidad en nuestros sitios. No importa el lenguaje de programación que utilicemos, con el simple hecho de realizar operaciones en el servidor ya estamos expuestos.

Tampoco es para preocuparse tanto, pero bueno, vamos allá.

Descargar ejemplo

Si quieres ver el ejemplo y probarlo por ti mismo, puedes descargar los archivos, configurar tu servidor local y listo.

Ve a GitHub para descargar el código completo: https://github.com/parzibyte/ataque_csrf_php

¿Qué es un ataque CSRF?

No voy a entrar en definiciones tan formales ni cosas de esas. Vamos a hacerlo simple.

Un ataque CSRF es aquel en donde un usuario malicioso, de cualquier manera, logra hacer que un usuario ingenuo realice una acción sin que éste último lo sepa, beneficiando de cualquier manera al primero.

Los casos más comunes son con un link (a veces cargados desde una imagen), aunque igualmente puede ser con un formulario en donde el botón para enviar se disfrace como un link.

Ve más abajo para ver ejemplos.

Ejemplos de ataques CSRF

El ejemplo más básico (aunque no creo que exista en la vida real) es aquel en donde hay un sitio web de un banco y para hacer la transferencia se utiliza un link. Por ejemplo, un enlace como este:

tubanco.com/transferir.php?cantidad=1000&para=parzibyte

Dicho enlace, cuando lo hace el usuario normal, no es peligroso. Pues el usuario sabe que está haciendo una transferencia.

Si un atacante descubre que con el simple hecho de visitar ese link se hace la transferencia, sabrá que con que el usuario ingenuo visite el link (o haga una petición GET al mismo) se realizará el movimiento.

En ese caso le harías una transferencia al usuario parzibyte. Dicho enlace no necesariamente tendría que ser clickeado, podría ser cargado dentro de una imagen que se encuentra en otro sitio. Así:

<img src="tubanco.com/transferir.php?cantidad=1000&para=parzibyte" style="display: none;">

La fuente de la imagen no es una imagen, pero el navegador va a hacer la petición a dicho enlace. Poco nos importa que la imagen cargue o no, ya que está oculta.

Ahora alguien oculta esa imagen en un sitio, te dice que lo visites y listo, sin saberlo ya has hecho la transferencia.

Hay muchos casos y ejemplos, desde los más básicos (como aprobar un comentario de tu blog, eliminar un producto de tu sistema de inventarios) hasta los más sofisticados como dar permisos a un nuevo usuario, eliminar tu propia cuenta, hacer una transferencia, entre otros.

Igualmente hay ataques CSRF por el método POST, es decir, que los valores y parámetros no se envíen en la ruta, sino por un formulario.

Ejemplo de ataque CSRF en PHP

Vamos a simular un pequeño banco. En los ejemplos de arriba, el remitente (o no sé cómo se le diga al que hace la transferencia) era tomado de la sesión, pero ahorita no veremos eso de las sesiones porque ese no es el punto.

Simplemente escribiremos en un fichero de texto los movimientos realizados, para darnos una idea.

Y para hacer una transferencia utilizaremos el link:

tubanco.com/transferir.php?cantidad=1000&para=parzibyte&desde=usuario_ingenuo

Programando el script que hace las transferencias

Comencemos con el código PHP que hará las transferencias. Será muy simple como lo dije, sólo escribirá en un archivo las transferencias que se hacen.

Si quieres profundizar en el tema de cómo escribir, leer y hacer un CRUD con ficheros te recomiendo leer: CRUD de ficheros en PHP

Queda así:

<?php
/**
*	Simular una transferencia de dinero entre usuarios para
*	explotarlo con un ataque CSRF
*
*	@author parzibyte.me/blog
*/
$desde = $_GET["desde"];
$para = $_GET["para"];
$cantidad = floatval($_GET["cantidad"]);
file_put_contents("transferencias.txt", "Se transfieren $cantidad desde $desde para $para" . PHP_EOL, FILE_APPEND);
echo "OK";
?>

Programando interfaz para el usuario

Esta será la interfaz legal o verdadera. Aquí es en donde se hacen las transferencias normalmente.

Para llamar al link de arriba (transferir.php) podemos usar un formulario, o un link. En este caso será este formulario:

<!DOCTYPE html>
<html lang="es">
<head>
	<meta charset="UTF-8">
	<title>Hacer transferencias de un banco</title>
</head>
<body>
	<form action="transferir.php">
		<input placeholder="Remitente" type="text" name="desde"><br>
		<input placeholder="Receptor" type="text" name="para"><br>
		<input placeholder="¿Cuánto transfieres?" type="number" name="cantidad"><br>
		<button type="submit">Realizar transferencia</button>
	</form>
</body>
</html>

Y cuando el usuario hace la transferencia, todo va bien, porque él sabe que lo está haciendo…

Rellena el formulario | Ejemplo de qué es un ataque CSRF
Rellena el formulario | Ejemplo de qué es un ataque CSRF
En el registro todo va bien, pues fue un movimiento que el usuario quería hacer
En el registro todo va bien, pues fue un movimiento que el usuario quería hacer

Hasta aquí todo va bien. Ahora vamos a explotar esa “vulnerabilidad” con otra página…

Ataque CSRF 🙂

Ahora viene lo bueno. Vamos a mandarle a ese usuario una página muy bonita y no sabrá que cargaremos este link para transferirnos 10000 pesos, dólares o el tipo de moneda que haya en tu país:

transferir.php?desde=ingenuo&para=parzibyte&cantidad=10000

Dicha página tendríamos que hacerla bonita y llamativa, o tal vez no si el usuario confía en nosotros. Lo haré rápido porque el objetivo es explicar el funcionamiento.

<!DOCTYPE html>
<html lang="es">
<head>
	<meta charset="UTF-8">
	<title>Mi página bonita</title>
</head>
<body>
	<h1>Bienvenido a mi sitio. Aquí puedes ver mi perfil</h1>
	<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quae quam nulla aperiam repellendus iure doloribus. Excepturi reiciendis minima, earum, sapiente sed mollitia totam expedita ut ad ea vel itaque doloremque.</p>
	<!-- Justo aquí abajo cargamos la imagen para el ataque CSRF :-) -->
	<img src="transferir.php?desde=ingenuo&para=parzibyte&cantidad=10000" style="width: 0; height: 0;">

	<!--La imagen de abajo sí es real y no hace nada-->
	<img src="https://picsum.photos/200" alt="">
</body>
</html>

Ahora le damos al usuario un enlace a nuestra página trampa. Él verá esto y no sospechará nada:

Página trampa para ejemplificar qué es un ataque CSRF
Página trampa para ejemplificar qué es un ataque CSRF

Como vimos, cargamos una imagen con un alto y ancho de 0. Podemos ocultarla como deseemos, eso es lo de menos. Lo que más me gusta es que si vamos ahora al log…

Oh sí. La transferencia fue hecha y el usuario ni cuenta se dio, sólo tuvo que entrar a nuestra página trampa.

Lo sé, es un ejemplo muy básico pero ahora imagina cuántas cosas se podrían hacer con este tipo de ataques, suponiendo que uno estudia bien el tema.

Cómo prevenir un ataque CSRF

Como lo dije, este ataque ya es viejo y ya existe una solución. Es lo que llamamos token CSRF y lo que hace es que genera una cadena aleatoria que no es predecible con el tiempo.

Ese token no lo sabrá el atacante, y al usuario se lo daremos en el formulario. Vamos paso por paso.

Por cierto, si queremos una alternativa a un token CSRF podemos utilizar un captcha que se supone es más seguro. Te invito a leer cómo implementar el captcha de coinhive.

Ejemplo de prevención de ataque CSRF

Generar token seguro

Ya hice un post de cómo generar un token criptográficamente seguro. Puedes implementar tu función si gustas, aunque no te lo recomiendo.

Inyectar token en sesión y en el formulario

El token seguro que generamos se lo daremos al usuario en el formulario, como un valor oculto. Pero antes de dárselo lo vamos a inyectar en la sesión así:

<?php
#Nota: esta función funciona en PHP7. Visita https://parzibyte.me/blog/2018/06/27/generar-token-criptograficamente-seguro-php/
# si tu versión es menor
function generar_token_seguro($longitud)
{
    if ($longitud < 4) {
        $longitud = 4;
    }
 
    return bin2hex(random_bytes(($longitud - ($longitud % 2)) / 2));
}

# Sólo iniciamos la sesión si no la iniciamos antes
if(session_status() !== PHP_SESSION_ACTIVE){
	session_start();
}
$token = generar_token_seguro(40); # La longitud puede variar pero yo la pondré en 40
$_SESSION["token"] = $token;
?>
<!DOCTYPE html>
<html lang="es">
<head>
	<meta charset="UTF-8">
	<title>Hacer transferencias de un banco</title>
</head>
<body>
	<form action="transferir.php">
		<!-- Aquí abajo inyectamos el token -->
		<input type="hidden" value="<?php echo $token; ?>" name="token_csrf">
		<input placeholder="Remitente" type="text" name="desde"><br>
		<input placeholder="Receptor" type="text" name="para"><br>
		<input placeholder="¿Cuánto transfieres?" type="number" name="cantidad"><br>
		<button type="submit">Realizar transferencia</button>
	</form>
</body>
</html>

El usuario seguirá viendo lo mismo, pero internamente estará el token:

Token que se genera en el formulario
Token que se genera en el formulario

Ese token no lo sabrá el atacante, pues se regenera cada que el usuario entra.

Comprobar token al realizar transferencia

Ahora en el archivo de transferencia primeramente vamos a comparar el token con el que tenemos en la sesión y la haremos en caso de que coincidan:

<?php
/**
*	Prevenir ataque CSRF con un token
*
*	@author parzibyte.me/blog
*/
$desde = $_GET["desde"];
$para = $_GET["para"];
$cantidad = floatval($_GET["cantidad"]);
$token = $_GET["token_csrf"];

#Aquí iniciamos la sesión si no está iniciada, esto es para leer el token que guardamos anteriormente
if(session_status() !== PHP_SESSION_ACTIVE){
	session_start();
}

#Comparamos tokens...
if(strcmp($token, $_SESSION["token"]) === 0){
	file_put_contents("transferencias.txt", "Se transfieren $cantidad desde $desde para $para" . PHP_EOL, FILE_APPEND);
	echo "OK";
}else{
	file_put_contents("transferencias.txt", "NO se ha transferido $cantidad desde $desde para $para porque el token no coincide!" . PHP_EOL, FILE_APPEND);
	echo "NO";
}
?>

Así queda el script. Por cierto, para comparar los tokens usamos strcmp que devuelve cero si las cadenas son idénticas. Podríamos igualmente usar hash_equals.

Transferencia segura

Ahora el usuario hace la transferencia y el link cambia a algo como:

transferir_seguro.php?token_csrf=35a719eb761cc5d86c0da53f0565a24692182f8b&desde=nadie&para=parzibyte&cantidad=500

Como esta transferencia fue legal o como se diga, en el log estará el registro.

Sólo queda intentar atacar de nuevo.

Renovar página trampa

Como vimos, ahora el archivo se llama transferir_seguro.php en lugar de transferir.php. Vamos a actualizarlo en la página trampa.

<!DOCTYPE html>
<html lang="es">
<head>
	<meta charset="UTF-8">
	<title>Mi página bonita</title>
</head>
<body>
	<h1>Bienvenido a mi sitio. Aquí puedes ver mi perfil</h1>
	<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quae quam nulla aperiam repellendus iure doloribus. Excepturi reiciendis minima, earum, sapiente sed mollitia totam expedita ut ad ea vel itaque doloremque.</p>
	<!-- Justo aquí abajo cargamos la imagen para el ataque CSRF :-) -->
	<img src="transferir_seguro.php?desde=ingenuo&para=parzibyte&cantidad=10000" style="width: 0; height: 0;">

	<!--La imagen de abajo sí es real y no hace nada-->
	<img src="https://picsum.photos/200" alt="">
</body>
</html>

Y si ahora la visitamos, todo sigue igual:

Pero ahora veamos el log:

Oh sí, hemos prevenido el ataque. Y el usuario malicioso nunca sabrá el token (excepto si es un adivino jaja), porque no puede calcularlo de ninguna manera.

Además, el token es distinto por cada usuario y se regenera en cada visita a la página.

Frameworks que ya lo usan

De algunos frameworks web que conozco, puedo decir que estos dos implementan protección contra ataques CSRF. Puede haber más, no lo sé, sería cosa de investigar.

  • Laravel
  • Django

Conclusión

Y así es como terminamos este post. Si tienes alguna duda ponla en los comentarios. Espero que haya quedado claro qué es un ataque CSRF.

Encantado de ayudarte


Estoy disponible para trabajar en tu proyecto, modificar el programa del post o realizar tu tarea pendiente, no dudes en ponerte en contacto conmigo.

No te pierdas ninguno de mis posts

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.

2 comentarios en “Qué es un ataque CSRF y cómo prevenirlo”

  1. Pingback: Presentando un sistema web para hacer cotizaciones y presupuestos, gratuito y open source - Parzibyte's blog

Dejar un comentario