Saltar al contenido

XSS a fondo: reflejado, almacenado, DOM y por qué la CSP no te salva sola

    XSS a fondo:

    reflejado, almacenado, DOM y por qué la CSP no te salva sola

    Seguridad de aplicaciones web  |  unfantasmaenelsistema.com

    El cross-site scripting lleva más de veinte años en la lista de OWASP y no se va. La razón no es que sea difícil de entender, sino que la defensa correcta depende del contexto exacto donde acaba el dato del usuario, y ese contexto cambia línea a línea. Un mismo valor puede ser inofensivo dentro de un párrafo y ejecutable tres caracteres más allá, dentro de un atributo.

    Este artículo explica XSS con enfoque conceptual y defensivo: los tres tipos, la idea de source/sink en el XSS basado en DOM, por qué el escape depende del contexto de salida, y las tres capas de defensa —codificación, sanitización y CSP— con sus límites reales. Sin catálogo de payloads de evasión: el objetivo es escribir código que no sea vulnerable, no romper el de otros.

    Qué aprenderás en este artículo

    Los tres tipos de XSS: reflejado, almacenado y basado en DOM · Qué son source y sink y por qué el XSS de DOM no se ve en el servidor · Los contextos de salida (HTML, atributo, JavaScript, URL, CSS) y su codificación · Cuándo hace falta sanitizar HTML rico y con qué · Qué aporta la CSP como segunda capa y qué NO puede cubrir · Trusted Types y cómo detectar patrones peligrosos en tu código

    Encuadre de pruebas

    Practica XSS solo en aplicaciones deliberadamente vulnerables y de tu entorno: OWASP Juice Shop, DVWA, WebGoat, o código propio en el laboratorio 192.168.56.x · Inyectar scripts en aplicaciones de terceros sin autorización escrita es acceso ilícito y puede causar daños (arts. 197 bis, 197 ter y 264 del Código Penal español); robar sesiones ajenas agrava · Si detectas un XSS en un servicio que usas, repórtalo por su canal de seguridad o vía INCIBE-CERT; no lo demuestres con la cuenta de otra persona

    1. Qué es XSS y por qué sigue vivo

    XSS ocurre cuando una aplicación web incorpora datos controlados por un usuario en una página sin el tratamiento adecuado para ese contexto, de modo que el navegador de la víctima acaba interpretando parte de esos datos como código (HTML o JavaScript) en lugar de como texto.

    El script se ejecuta con el origen de la aplicación vulnerable: puede leer el DOM, robar tokens de sesión accesibles, hacer peticiones autenticadas en nombre del usuario, modificar lo que se ve y capturar lo que se teclea. Sigue vivo porque las aplicaciones modernas construyen HTML en el cliente, con decenas de puntos donde un dato se convierte en marcado, y basta fallar en uno.

    2. Los tres tipos

    TipoDónde vive el payloadCómo llega a la víctima
    ReflejadoEn la petición (parámetro, ruta) y se devuelve en la respuestaUn enlace manipulado que la víctima abre
    AlmacenadoGuardado en el servidor (comentario, perfil, nombre)La víctima visita una página normal que muestra ese dato
    Basado en DOMNunca toca el servidor: lo procesa JavaScript en el clienteUn fragmento de URL (#…) o un valor local que el JS mete en un sink

    El almacenado es el más grave (afecta a todo el que vea la página, a veces sin interacción). El basado en DOM es el más difícil de auditar: no aparece en los logs del servidor porque la inyección sucede entera en el navegador.

    2.1 Source y sink en el XSS de DOM

    Un source es un origen de datos que el atacante puede influir: location.hash, location.search, document.referrer, postMessage, localStorage. Un sink es una API que convierte una cadena en código o marcado: innerHTML, outerHTML, document.write, eval, setTimeout con cadena, insertAdjacentHTML, la asignación a src de un script. El XSS de DOM es un source que llega a un sink sin saneamiento por el medio.

    3. El contexto lo es todo

    El mismo dato necesita un tratamiento distinto según dónde se inserte en la página. Escapar para HTML y luego colocarlo dentro de un bloque de JavaScript no protege de nada.

    Contexto de salidaCodificación / tratamiento correctoEjemplo de destino
    Cuerpo HTMLCodificar < > & (entidades HTML)<p>AQUI</p>
    Atributo HTML entrecomilladoCodificar comillas y & ; siempre entre comillas<input value=»AQUI»>
    Nombre/valor de atributo sin comillasEvitarlo; es casi imposible de asegurar<div AQUI>
    Dentro de <script> o de un manejador on*No insertar datos ahí; si es inevitable, serializar como JSON seguro<script>var x = AQUI</script>
    URL en href / srcValidar el esquema (solo http/https), codificar por componente<a href=»AQUI»>
    CSS (style, hoja)No insertar datos de usuario en CSSstyle=»width:AQUI»

    La conclusión práctica: decide el contexto antes de escribir la plantilla y usa la codificación de ese contexto. Y evita los contextos imposibles de asegurar (datos en JavaScript en línea, en CSS, en atributos sin comillas).

    4. Defensa 1: codificación de salida y autoescape

    La defensa primaria es codificar en el momento de la salida, según el contexto. Los motores de plantillas modernos (React con JSX, Angular, Vue, Django, Jinja2 con autoescape, Thymeleaf, Razor) escapan por defecto el contexto HTML: si pasas una cadena, se ve como texto, no como marcado.

    El peligro está en las salidas de escape explícito: dangerouslySetInnerHTML en React, v-html en Vue, |safe en Jinja2, bypassSecurityTrustHtml en Angular, element.innerHTML en JavaScript puro. Cada uso de esas APIs es un punto que hay que justificar y revisar.

    // PELIGROSO: interpreta el valor como HTML
    el.innerHTML = nombreUsuario;
     
    // SEGURO: lo trata como texto, el navegador no ejecuta nada
    el.textContent = nombreUsuario;
     
    // Insertar un nodo de texto tambien es seguro
    el.append(document.createTextNode(nombreUsuario));

    5. Defensa 2: sanitizar HTML rico

    A veces el requisito es legítimo: un editor de texto enriquecido, comentarios con formato, contenido de un CMS. Ahí no puedes escapar todo (perderías el formato) y necesitas sanitizar: parsear el HTML y quedarte solo con una lista blanca de etiquetas y atributos seguros, eliminando scripts, manejadores de eventos y URLs peligrosas.

    • En el navegador: DOMPurify es el estándar de facto, mantenido y auditado.
    • En el servidor: sanitizadores equivalentes por lenguaje (por ejemplo, sanitize-html en Node, bleach en Python, OWASP Java HTML Sanitizer).
    • La Sanitizer API nativa del navegador está llegando, pero aún conviene la biblioteca por cobertura.
    • Regla: sanitiza al renderizar, no solo al guardar. Si guardas HTML «limpio» y mañana cambia la lista de bypass conocidos, el dato viejo sigue ahí.

    6. Defensa 3: CSP como segunda capa (y sus límites)

    La Content Security Policy es una cabecera que le dice al navegador de qué orígenes puede cargar y ejecutar recursos. Bien hecha, convierte muchos XSS en fallos sin impacto: aunque se inyecte un script, el navegador se niega a ejecutarlo.

    La CSP que de verdad frena XSS no es la de listas blancas de dominios (se saltan con gadgets y endpoints permisivos dentro de un dominio permitido), sino la basada en nonce o hash con strict-dynamic:

    Content-Security-Policy:
      script-src 'nonce-r4nd0m2026' 'strict-dynamic';
      object-src 'none';
      base-uri 'none';
      require-trusted-types-for 'script';
     
    <!-- Solo se ejecuta el script que lleva el nonce del servidor -->
    <script nonce="r4nd0m2026"> /* codigo propio */ </script>

    Lo que la CSP no cubre

    No frena la exfiltración por canales permitidos (una imagen a un dominio de tu lista, prefetch, DNS) · No protege si permites ‘unsafe-inline’ o ‘unsafe-eval’ — y eso invalida casi toda la protección · No arregla el XSS, lo mitiga: si la política tiene un hueco, el fallo vuelve. Es una red, no un suelo · Requiere trabajo de despliegue: empezar en modo Report-Only y revisar los informes antes de forzar

    7. Trusted Types y detección

    Trusted Types (con require-trusted-types-for ‘script’ en la CSP) obliga a que los sinks peligrosos del DOM solo acepten objetos creados por una política explícita, no cadenas sueltas. En la práctica elimina el XSS basado en DOM por diseño: el código que asigna una cadena a innerHTML deja de funcionar hasta que pasa por un sanitizador declarado. Es la defensa más fuerte disponible hoy para aplicaciones nuevas.

    7.1 Detección en código y en ejecución

    • Análisis estático (SAST) y linters: reglas que marcan innerHTML, document.write, eval, dangerouslySetInnerHTML, |safe, v-html para revisión manual.
    • Revisión de plantillas: cada punto de escape explícito necesita un comentario que justifique por qué es seguro ahí.
    • DAST y fuzzing contra entornos de prueba propios: inyectar marcadores inertes y ver si vuelven sin codificar.
    • En producción: los informes de violación de CSP (report-uri / report-to) son una señal temprana de intentos de inyección.
    • Cabeceras: X-Content-Type-Options: nosniff y un Content-Type correcto evitan que una respuesta de API se interprete como HTML.

    Referencias y recursos

    OWASP XSS Prevention Cheat Sheet y DOM-based XSS Prevention Cheat Sheet · OWASP Juice Shop y DVWA para practicar en local de forma legal · github.com/cure53/DOMPurify — sanitizador y su wiki de configuración · web.dev y w3c.github.io/trusted-types — guía de Trusted Types y CSP estricta · csp-evaluator.withgoogle.com — revisar la fortaleza de una política CSP

    Conclusión

    XSS no se arregla con una única medida porque no es un único fallo: es una familia de errores que comparten causa —datos de usuario tratados como código— y difieren en el contexto. La defensa primaria y no negociable es la codificación de salida correcta para cada contexto, apoyada en plantillas con autoescape y en sanitización con lista blanca cuando hace falta HTML rico.

    La CSP con nonce y strict-dynamic, y sobre todo Trusted Types, son la segunda capa que reduce el impacto cuando la primera falla. Pero son red de seguridad, no cimiento: una CSP con ‘unsafe-inline’ o una lista blanca laxa da una falsa sensación de protección. El código que no genera el fallo sigue siendo el objetivo.

    unfantasmaenelsistema.com | Seguridad, sistemas y todo lo que no debería funcionar (pero funciona)

    Etiquetas:
    Resumen de privacidad

    Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.