XSS Game

Google xss-game.appspot.com — 6 niveles de Cross-Site Scripting: Reflected, Stored, DOM-based y bypass de protocolo

● FÁCIL GOOGLE XSS WEB JAVASCRIPT 6/6 NIVELES 📅 Junio 2026 · By Brandeiks

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:).

NivelNombreTipo de XSS
1Hello, world of XSSReflejado — sin escapado de HTML
2Persistence is keyAlmacenado — bypass de filtro de <script>
3That sinking feelingDOM-based — sink oculto (src de imagen)
4Context mattersInyección en contexto de código JavaScript
5Breaking protocolEsquema javascript: sin validar
6Follow the white rabbitCarga dinámica de script — bypass con data:

Nivel 1 — Hello, world of XSS

1
XSS REFLEJADO BÁSICO

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 &lt;, 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.

PAYLOAD
$ <script>alert(1)</script>

// 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.


Nivel 2 — Persistence is key

2
XSS ALMACENADO + BYPASS DE FILTRO

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.

PAYLOAD
$ "><svg onload=alert(1)>

// 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.


Nivel 3 — That sinking feeling

3
DOM-BASED XSS — SINK OCULTO

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.

PAYLOAD (URL)
$ https://xss-game.appspot.com/level3/frame#2' onerror='alert("XSS")'

// 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.


Nivel 4 — Context matters

4
INYECCIÓN EN CONTEXTO DE CÓDIGO JS

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.

PAYLOAD
$ 3'); alert('1

// 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.


Nivel 5 — Breaking protocol

5
ESQUEMA javascript: SIN VALIDAR

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.

PAYLOAD (URL)
$ https://xss-game.appspot.com/level5/frame/signup?next=javascript:alert('XSS')

// 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.


Nivel 6 — Follow the white rabbit

6
CARGA DINÁMICA DE SCRIPT — BYPASS CON data:

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.

PAYLOAD (URL)
$ https://xss-game.appspot.com/level6/frame#data:text/plain,alert()

// 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.


Tabla de Payloads

NivelPayloadVector
1<script>alert(1)</script>Inyección directa de etiqueta
2"><svg onload=alert(1)>Escape de atributo + evento SVG
3https://xss-game.appspot.com/level3/frame#2' onerror='alert("XSS")'Sink oculto en src de imagen
43'); alert('1Escape de contexto JS (string + función)
5https://xss-game.appspot.com/level5/frame/signup?next=javascript:alert('XSS')Esquema javascript: en href
6https://xss-game.appspot.com/level6/frame#data:text/plain,alert()Esquema data: no filtrado

Glosario

XSS — Cross-Site Scripting
Vulnerabilidad que permite a un atacante inyectar código JavaScript en una página web vista por otros usuarios, ejecutándose con los mismos privilegios y acceso que el sitio legítimo (cookies, sesión, DOM). Está en el Top 10 de OWASP desde hace más de una década.
XSS Reflejado (Reflected)
El payload viaja en la propia petición (normalmente en parámetros de la URL) y el servidor lo refleja inmediatamente en la respuesta sin almacenarlo. Requiere que la víctima haga clic en un enlace especialmente diseñado.
XSS Almacenado (Stored)
El payload se guarda de forma persistente en el servidor (base de datos, archivo, comentario, post) y se ejecuta cada vez que cualquier usuario visualiza ese contenido — más peligroso porque no requiere interacción específica con un enlace.
DOM-based XSS
La vulnerabilidad ocurre completamente en el navegador: JavaScript del lado cliente toma datos no confiables (como location.hash o document.URL) y los escribe de forma insegura en el DOM, sin que el servidor llegue a ver el payload.
Sink (Sumidero)
Punto en el código donde un dato controlado por el atacante termina siendo interpretado como código o marcado activo — por ejemplo innerHTML, eval(), document.write(), o la asignación a .src/.href.
Escapado (Output Encoding)
Técnica de convertir caracteres con significado especial en el contexto de salida (HTML, atributo, JS, URL, CSS) en su representación segura — por ejemplo <&lt; — para que el navegador los trate como texto literal y no como código.
Blacklist vs. Whitelist
Una blacklist bloquea patrones específicos conocidos como peligrosos (ej. la palabra <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.
CSP — Content Security Policy
Cabecera HTTP que permite a un sitio restringir desde qué orígenes puede cargarse JavaScript, prohibir eval() e inline scripts, mitigando el impacto de XSS incluso si una vulnerabilidad de inyección logra colarse en el código.