Aprende a evitar ataques XSS y CSRF

¿Que es un ataque XSS – Cross Site Scripting?

Un ataque XSS o Cross-Fide Scripting, consiste en la posibilidad de introducir código HTML / JavaScript malicioso dentro de las cajas de un formulario de una web. Si dicho formulario no controla los datos que recibe, es posible que el código malicioso se ejecute en la web y de esta manera poder robar datos, contraseñas, hacer redirecciones … etc.

¿Cómo podernos protegernos de este tipo de ataques?

Si usamos PHP existen dos funciones que nos hacen el trabajo sucio, estas funciones son htmlspecialchars() y htmlentities(), esta segunda si nuestra página no usa algunas de las codificaciones de caracteres ISO-8859-1 o UTF-8. Para proteger el contenido que se almacena en una tabla usaremos la funcion strip-tags().

Ejemplos htmlspecialchars() y htmlentities():
echo htmlspecialchars('<script>alert("El niño atacó esta web");</script>');
// resultado: &lt;script&gt;alert(&quot;El niño atacó esta web&quot;);&lt;/script&gt;

echo htmlentities('<script>alert("El niño atacó esta web");</script>');
// resultado: &lt;script&gt;alert(&quot;El ni&ntilde;o atac&oacute; esta web&quot;);&lt;/script&gt;
Ejemplo strip-tags():
echo strip_tags('<script>window.location = "http://www.google.com";</script>');

// resultado que se almacenaría: alert("El niño atacó esta web");

Controles más exhaustivos…

Si lo que queremos es un control más exhaustivo de lo que queremos filtrar, podremos hacer uso de la clase de PHP Input Filter, la cuál nos permite hacer el filtrado de datos de forma sencilla, empleando los métodos proporcionados.

Para descargarse esa clase lo podéis hacer (previo registro) desde: http://www.phpclasses.org/package/2189-PHP-Filter-out-unwanted-PHP-Javascript-HTML-tags-.html

Para usar la clase tendremos que insertarla en nuestro documento:

require_once 'class.inputfilter.php';
$filtro = new InputFilter();
$nombre = $filtro->process($_POST['nombre']);

Si lo que queremos es filtrar todos los datos recibidos por el método POST, pasaremos el array $_POST.

Por ejemplo:

require_once 'class.inputfilter.php';
$filtro = new InputFilter();
$_POST = $filtro->process($_POST);

También se le puede indicar a la clase, si queremos que no filtre algunas etiquetas para que no las elimine del contenido, para ello haremos:

require_once 'class.inputfilter.php';
$filtro = new InputFilter(array('strong','bold'));
$nombre = $filtro->process($_POST['nombre']);

Incluso podremos también indicar atributos que no queremos que filtre. Para ello lo que hacemos es definir un segundo array con los atributos que no queremos que filtre.

require_once 'class.inputfilter.php';
$filtro = new InputFilter(array('a'), array('href'));
$comentarios = $filtro->process($_POST['comentarios']);

Información de atributos a usar durante la instanciación del objeto InputFilter.

1º (array etiquetas): Opcional (desde la versión 1.2.0)
2º (array atributos): Opcional
3º (métodos etiquetas): 0 = eliminar TODO EXCEPTO estas etiquetas (por defecto)
1 = eliminar SOLO estas etiquetas
4º (métodos atributos): 0 = eliminar TODO EXCEPTO estos atributos (por defecto)
1 = eliminar SOLO estos atributos
5º (xss autostrip): 1 = eliminar todos los problemas con etiquetas (por defecto)
0 = desactivar esta característica

Ejemplo:

$mifiltro = new InputFilter($tags, $attributes, 0, 0);

¿Que es una ataque CSFR – Cross Site Request Forgery?

El CSRF o XSRF (del inglés Cross-site Request Forgery o falsificación de petición en sitios cruzados) es un tipo de exploit malicioso de un sitio web en el que se consigue que se ejecuten comandos no autorizados, por un usuario autorizado del sitio web, sin que éste lo pretenda. Esta vulnerabilidad es conocida también por otros nombres como XSRF, enlace hostil, ataque de un click, cabalgamiento de sesión y ataque automático.

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.

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.

Fuente de información principal:

https://manuais.iessanclemente.net/index.php/Evitar_ataques_XSS_y_CSRF_con_PHP

Gracias al autor original del contenido.

Hasta la próxima!! 😉

About: Miguel Carretas Perulero

Miguel Carretas Perulero ha escrito 61 artículos en este blog.

Deja un comentario