// 01
Resumen
El XSS Game es un laboratorio educativo creado y mantenido por Google, alojado en xss-game.appspot.com, diseñado específicamente para enseñar de forma práctica los distintos tipos de Cross-Site Scripting (XSS). Cada uno de los 6 niveles presenta una mini-aplicación web vulnerable distinta, y el objetivo siempre es el mismo: lograr que el navegador ejecute un alert() de JavaScript inyectado por el jugador.
Lo interesante de este laboratorio es que cada nivel introduce una variante distinta del mismo problema de fondo — datos no confiables que terminan ejecutándose como código. Se recorre el espectro completo de XSS: reflejado, almacenado, basado en DOM y bypass de filtros y protocolos (javascript: y data:).
| Nivel | Nombre | Tipo de XSS |
|---|---|---|
| 1 | Hello, world of XSS | Reflejado — sin escapado de HTML |
| 2 | Persistence is key | Almacenado — bypass de filtro de <script> |
| 3 | That sinking feeling | DOM-based — sink oculto (src de imagen) |
| 4 | Context matters | Inyección en contexto de código JavaScript |
| 5 | Breaking protocol | Esquema javascript: sin validar |
| 6 | Follow the white rabbit | Carga dinámica de script — bypass con data: |
// 02
Nivel 1 — Hello, world of XSS
El primer nivel es la introducción clásica al XSS reflejado: el campo de búsqueda de la
aplicación FourOrFour toma el valor escrito por el usuario y lo inserta
directamente en el HTML de la página de resultados, sin ningún tipo de
escapado (sin convertir < en <, por ejemplo).
Como el navegador no distingue entre "HTML legítimo de la página" y "HTML que llegó desde el input del usuario", al escribir una etiqueta <script> el parser la interpreta exactamente igual que si hubiera estado en el código fuente original, y ejecuta el JavaScript contenido en ella.
// POR QUÉ FUNCIONA
El servidor responde con algo como <p>Resultados para: [INPUT]</p>. Si INPUT no se sanitiza, el atacante simplemente cierra el contexto que necesite y abre una etiqueta ejecutable. Es la causa raíz de XSS más común en aplicaciones que generan HTML manualmente.
// 03
Nivel 2 — Persistence is key
Madchattr es un mini muro de publicaciones tipo chat: lo que se escribe se guarda en el servidor y se vuelve a mostrar cada vez que la página se recarga. Eso lo convierte en un escenario de XSS almacenado (stored) — el payload no necesita ser reenviado por el atacante cada vez, queda persistido.
La aplicación sí filtra la etiqueta <script> de forma
literal, pero sigue insertando el resto del input dentro de un atributo HTML existente sin
escapar comillas. Cerrando el atributo con " y la etiqueta con >,
se puede abrir un elemento totalmente distinto — en este caso un
<svg> — que no está en la lista negra del filtro pero
sí soporta el manejador de evento onload, el cual el navegador
ejecuta automáticamente al renderizar el SVG.
// LECCIÓN CLAVE
Las blacklists de etiquetas (bloquear solo <script>) son insuficientes: HTML tiene decenas de elementos y eventos (onload, onerror, onmouseover...) capaces de ejecutar JavaScript sin necesitar <script> en absoluto.
// 04
Nivel 3 — That sinking feeling
cloudiddly muestra una galería de imágenes donde el número de imagen se lee
del fragmento de la URL (la parte después de #), que
nunca se envía al servidor — todo el procesamiento ocurre en JavaScript dentro del
navegador. Esto es un DOM-based XSS: la vulnerabilidad vive
enteramente en el código cliente.
Internamente, el script construye la ruta de la imagen con algo equivalente a
img.src = 'image' + num + '.jpg'. Como num proviene directamente
del hash sin validar, se puede inyectar una comilla simple para cerrar el atributo
src y añadir un nuevo atributo de evento. Al apuntar a una
ruta de imagen que no existe, se dispara onerror.
// SINK OCULTO
El nivel se llama "that sinking feeling" porque el sink (el punto donde el dato controlado por el atacante termina ejecutándose) está escondido detrás de una API de alto nivel — aquí, la asignación a .src de una imagen — y no es obvio a primera vista que sea peligroso.
// 05
Nivel 4 — Context matters
timemer toma el valor del campo de temporizador y lo inserta dentro de una
llamada a función JavaScript ya existente en la página, de forma equivalente a
startTimer('VALOR'). Aquí el contexto de inyección ya no es HTML, sino
código JavaScript en sí mismo — y por eso el escapado necesario es
completamente distinto al de los niveles anteriores.
Para escapar de la cadena de texto y de la llamada a función, basta con cerrar la comilla
simple y el paréntesis de la llamada original (');), lo que termina la
instrucción startTimer(...) de forma válida. A partir de ahí, cualquier código
que se escriba se ejecuta como una sentencia JavaScript completamente nueva e
independiente.
// POR QUÉ "CONTEXT MATTERS"
El payload correcto depende enteramente de dónde cae el input dentro del documento: dentro de texto HTML, dentro de un atributo, dentro de una cadena JS, dentro de una URL, dentro de CSS... cada contexto exige una técnica de escape diferente.
// 06
Nivel 5 — Breaking protocol
Groovy Reader redirige al usuario a una página de registro usando un parámetro next en la URL, que se inyecta directamente como el valor del atributo href de un enlace "Sign up". El nivel demuestra que el XSS no siempre requiere inyectar nuevas etiquetas: a veces basta con controlar el destino de un enlace o redirección ya existente.
Si la aplicación no valida que el valor de next sea una URL http(s) legítima,
el navegador acepta igualmente el esquema javascript: como destino
válido de un enlace. Al hacer clic, en lugar de navegar a otra página, el navegador ejecuta
el código que sigue al esquema como JavaScript.
// VALIDACIÓN DE PROTOCOLO
Cualquier campo que termine usado como href, src o redirección debe validarse contra una lista blanca de esquemas (http:, https:, mailto:...), nunca solo contra una lista negra de palabras prohibidas.
// 07
Nivel 6 — Follow the white rabbit
El nivel final, Glove Gadgets, carga dinámicamente un script externo
usando una ruta derivada del fragmento de la URL
(location.hash), por ejemplo para cargar "gadgets" adicionales. Es el nivel
más avanzado porque combina DOM-based XSS con un mecanismo de carga de scripts realista
—parecido a cómo cargan plugins muchas aplicaciones complejas.
La aplicación intenta bloquear esquemas peligrosos filtrando explícitamente
http:// y https://, asumiendo que así evita cargar scripts de
dominios externos no confiables. Sin embargo, no filtra el esquema
data:, que permite embeber contenido (incluyendo código JavaScript) directamente
dentro de la propia URL, sin necesidad de ningún servidor externo.
// LISTA NEGRA INCOMPLETA
Este nivel resume la lección de todo el laboratorio: filtrar por palabras prohibidas (blacklist) casi siempre es insuficiente. Existen múltiples esquemas de URL (data:, javascript:, vbscript: en navegadores antiguos) y vectores de HTML capaces de lograr el mismo resultado por caminos distintos.
// 08
Tabla de Payloads
| Nivel | Payload | Vector |
|---|---|---|
| 1 | <script>alert(1)</script> | Inyección directa de etiqueta |
| 2 | "><svg onload=alert(1)> | Escape de atributo + evento SVG |
| 3 | https://xss-game.appspot.com/level3/frame#2' onerror='alert("XSS")' | Sink oculto en src de imagen |
| 4 | 3'); alert('1 | Escape de contexto JS (string + función) |
| 5 | https://xss-game.appspot.com/level5/frame/signup?next=javascript:alert('XSS') | Esquema javascript: en href |
| 6 | https://xss-game.appspot.com/level6/frame#data:text/plain,alert() | Esquema data: no filtrado |
// 09
Glosario
location.hash o document.URL) y los escribe de forma insegura en el DOM, sin que el servidor llegue a ver el payload.innerHTML, eval(), document.write(), o la asignación a .src/.href.< → < — para que el navegador los trate como texto literal y no como código.<script>), pero es fácil de evadir con vectores alternativos. Una whitelist solo permite explícitamente lo que se sabe seguro — enfoque mucho más robusto contra XSS.eval() e inline scripts, mitigando el impacto de XSS incluso si una vulnerabilidad de inyección logra colarse en el código.