Seguridad en React: guía práctica contra XSS y vulnerabilidades comunes
Guía de seguridad React en español: XSS, dangerouslySetInnerHTML, componentes de terceros, CSP y mejores prácticas para hacer tu app React realmente segura.
Rod
Founder & Developer
Seguridad en React es un tema que muchos developers asumen resuelto. "React escapa automáticamente, estoy protegido." Esto es parcialmente verdad — y la parte que no es verdad es donde están las vulnerabilidades reales.
React sí previene XSS en casos básicos. Pero hay patrones comunes que rompen esa protección. Esta guía cubre exactamente cuáles son y cómo manejarlos.
Por qué React no es "automáticamente seguro"
React escapa el contenido que renderizas en JSX. Esto previene el caso más básico de XSS:
// Esto es seguro — React escapa el HTML
const userInput = '<script>alert("xss")</script>';
return <div>{userInput}</div>; // renderiza el texto literal, no el scriptPero hay cuatro lugares donde esa protección no aplica.
El riesgo más obvio: dangerouslySetInnerHTML
El nombre ya lo avisa. dangerouslySetInnerHTML inserta HTML directamente en el DOM sin escapar — exactamente lo que React evita por defecto.
// MAL: XSS directo si userContent viene del usuario
function BlogPost({ content }) {
return <div dangerouslySetInnerHTML={{ __html: content }} />;
}
// BIEN: sanitiza antes de insertar
import DOMPurify from "dompurify";
function BlogPost({ content }) {
const clean = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ["p", "b", "i", "em", "strong", "a"],
ALLOWED_ATTR: ["href"],
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}Instala dompurify con npm install dompurify. Es la biblioteca estándar para sanitización en el browser. Para SSR (Next.js) necesitas también isomorphic-dompurify:
npm install dompurify isomorphic-dompurify
npm install --save-dev @types/dompurifyEl riesgo que nadie ve: URLs dinámicas con href
React escapa el contenido de texto pero no valida los valores de atributos href. El scheme javascript: ejecuta código cuando el usuario hace click.
// MAL: si url viene del usuario, puede ser "javascript:alert(document.cookie)"
function UserLink({ url, label }) {
return <a href={url}>{label}</a>;
}
// BIEN: valida que la URL sea http o https
function UserLink({ url, label }) {
const isValidUrl = url.startsWith("https://") || url.startsWith("http://");
if (!isValidUrl) return <span>{label}</span>;
return <a href={url} rel="noopener noreferrer" target="_blank">{label}</a>;
}Nota el rel="noopener noreferrer" en links externos. Evita que la página destino acceda a tu página a través de window.opener.
eval() y Function() — los clásicos de siempre
// MAL: eval con datos externos es ejecución de código arbitrario
const result = eval(userProvidedExpression);
// MAL: igual de peligroso que eval
const fn = new Function("x", userProvidedCode);
// BIEN: si necesitas evaluar expresiones, usa una biblioteca segura
// como math.js para expresiones matemáticas
import { evaluate } from "mathjs";
const result = evaluate(userExpression); // sandbox seguroSi ves eval() en tu código o en una dependencia que recibe input del usuario, ese es un hallazgo que merece atención inmediata.
Componentes de terceros — el riesgo que no ves venir
Tu código puede estar perfecto y aun así tener una vulnerabilidad XSS a través de un componente de terceros que renderiza HTML sin sanitizar.
Los casos comunes:
- Editores de texto enriquecido (Quill, TipTap, Draft.js) — algunos sanitizan, otros no
- Componentes de markdown que convierten a HTML sin sanitización
- Librerías de charts que aceptan labels de usuario y los renderizan como HTML
// Verifica cómo maneja el HTML el componente que usas
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
// MAL: react-markdown por defecto no sanitiza HTML en el markdown
<ReactMarkdown>{userMarkdown}</ReactMarkdown>
// BIEN: deshabilita HTML crudo o usa rehype-sanitize
import rehypeSanitize from "rehype-sanitize";
<ReactMarkdown rehypePlugins={[rehypeSanitize]}>{userMarkdown}</ReactMarkdown>Content Security Policy (CSP) — la red de seguridad
Una CSP bien configurada es el backup. Incluso si una vulnerabilidad XSS existe en tu código, una CSP fuerte puede evitar que el script malicioso se ejecute o envíe datos afuera.
En Next.js:
// next.config.js
const ContentSecurityPolicy = `
default-src 'self';
script-src 'self' 'nonce-{nonce}';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.tuapp.com;
frame-ancestors 'none';
`;
module.exports = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "Content-Security-Policy",
value: ContentSecurityPolicy.replace(/\n/g, ""),
},
],
},
];
},
};El CSP más restrictivo posible es default-src 'none' y luego vas abriendo solo lo que necesitas. En la práctica, la mayoría de apps necesitan 'unsafe-inline' para estilos — eso es aceptable. Para scripts, intenta evitar 'unsafe-inline' y usa nonces en su lugar.
Puedes revisar si tu app tiene CSP configurado en tu URL de producción.
Estado en localStorage — qué guardar y qué no
localStorage es accesible desde cualquier script en tu dominio. Si tienes XSS, el atacante puede leer todo lo que esté ahí.
// MAL: tokens de autenticación en localStorage
localStorage.setItem("authToken", token); // vulnerable a XSS
// BIEN: cookies HttpOnly para autenticación
// El servidor las setea, JavaScript no puede leerlas
// Esto aplica al backend, no al componente React:
res.cookie("session", token, {
httpOnly: true,
secure: true,
sameSite: "strict",
});En localStorage sí puedes guardar: preferencias de UI, configuración no sensible, estado de onboarding. Lo que no debe estar ahí: tokens de autenticación, información de pagos, datos personales sensibles.
El checklist de seguridad para tu app React
Antes de hacer deploy, revisa esto:
- No hay
dangerouslySetInnerHTMLcon contenido del usuario sin sanitizar - Los links dinámicos validan que la URL sea http/https
- No hay
eval()con datos externos - Los componentes de markdown y editores de texto tienen sanitización
- CSP está configurado en los headers de respuesta
- Los tokens de auth están en cookies HttpOnly, no en localStorage
- Los paquetes de terceros están actualizados (
npm audit)
Un scanner automatizado puede detectar la mayoría de estos patrones. Si quieres ver cuántos tiene tu repo, el análisis toma menos de 5 minutos.
Las vulnerabilidades JavaScript más comunes van más allá de React — si tu app también tiene lógica de servidor, esa guía cubre los patrones de backend.
Preguntas frecuentes
¿React protege automáticamente contra XSS?
React escapa automáticamente los valores que renderizas en JSX, lo que previene XSS en la mayoría de casos. Sin embargo, esto no aplica cuando usas dangerouslySetInnerHTML, cuando construyes URLs dinámicamente con el scheme javascript:, o cuando insertas contenido HTML de fuentes externas sin sanitizar.
¿Cuándo es seguro usar dangerouslySetInnerHTML en React?
Cuando el HTML proviene de una fuente que controlas completamente (tú lo generaste, no el usuario), o cuando sanitizas el contenido con DOMPurify antes de renderizarlo. Nunca uses dangerouslySetInnerHTML directamente con input del usuario sin pasar por una biblioteca de sanitización.
¿Qué es XSS y cómo afecta a las apps React?
XSS (Cross-Site Scripting) ocurre cuando código JavaScript malicioso se ejecuta en el browser del usuario. En React, los vectores más comunes son dangerouslySetInnerHTML con contenido del usuario, href con valores javascript:, y eval() con datos externos.
¿Cómo configuro Content Security Policy en una app React?
En Next.js se configura en next.config.js con el header Content-Security-Policy. Para Create React App o Vite, se configura en el servidor web o en el meta tag del HTML. La CSP más básica útil restringe de dónde puede cargarse JavaScript, evitando la ejecución de scripts externos maliciosos.
¿Los componentes de React de terceros pueden ser un riesgo de seguridad?
Sí. Un paquete npm comprometido puede introducir código malicioso en tu app. La mitigación es revisar los componentes de terceros antes de instalarlos, usar lockfiles para fijar versiones, y escanear regularmente tus dependencias con herramientas como npm audit o Data Hogo.
Posts Relacionados
Variables de Entorno en Vercel: Qué es Seguro y Qué No
Vercel variables de entorno seguridad — qué expone NEXT_PUBLIC_, cómo configurar env por entorno, y cómo proteger secretos en edge functions. Con ejemplos reales.
Headers de Seguridad en Next.js: Guía Completa (2026)
Configura los headers de seguridad en Next.js — CSP, HSTS, X-Frame-Options y más. Ejemplos reales en next.config.ts y por qué cada header importa.
Firebase Security Rules: Los Errores Más Comunes (y Cómo Evitarlos)
firebase rules errores que dejan tus datos expuestos: read: true global, sin validación de datos, reglas que nunca aplican. Con ejemplos y fixes reales.