Auditoría de seguridad de React: guía completa para desarrolladores
Introducción
React ha dominado el desarrollo frontend durante casi una década, pero persiste un mito peligroso: que React es "seguro por defecto". En 2026, con el aumento de los ataques a la cadena de suministro y unas técnicas de evasión de CSP cada vez más sofisticadas, esta idea errónea deja vulnerables a innumerables aplicaciones. El escape de JSX de React protege contra algunos XSS, pero no hace nada contra los escollos del renderizado del lado del servidor, las vulnerabilidades de dependencias, las cabeceras de seguridad mal configuradas o los fallos de lógica de negocio. Una sola dependencia sin revisar o un dangerouslySetInnerHTML mal colocado puede exponer datos de usuarios. Esta guía recorre una auditoría de seguridad completa de React: prevención de XSS, configuración de CSP, escaneo de dependencias, aplicación del OWASP Top 10 y buenas prácticas de autenticación. Cada sección incluye ejemplos de código reales y correcciones accionables.
1. Prevención de XSS en React
El peligro de dangerouslySetInnerHTML
React nombra dangerouslySetInnerHTML deliberadamente para advertirte. Omite el escape integrado de React e inserta HTML en bruto en el DOM. Úsalo solo al renderizar HTML de confianza — y nunca con entrada de usuario.
Código vulnerable:
function Comment({ body }: { body: string }) {
return <div dangerouslySetInnerHTML={{ __html: body }} />;
}
// User submits: <img src=x onerror="fetch('/api/steal?cookie='+document.cookie)">
Corregido con DOMPurify:
import DOMPurify from "dompurify";
function Comment({ body }: { body: string }) {
const clean = DOMPurify.sanitize(body);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
DOMPurify elimina las construcciones maliciosas mientras conserva el HTML seguro. Configúralo siempre con reglas estrictas:
DOMPurify.sanitize(input, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "code", "pre"],
ALLOWED_ATTR: ["href", "class"],
ALLOW_DATA_ATTR: false,
});
La protección integrada de React
JSX escapa automáticamente los valores en {} — las cadenas se convierten en textContent, no en innerHTML. Esto previene la inyección en el caso común:
function SafeGreeting({ name }: { name: string }) {
return <div>{name}</div>; // Safe: even "<script>alert(1)</script>" is text
}
Pero cuidado con props como href, src o style, donde React no escapa de la misma manera:
function UserLink({ url }: { url: string }) {
return <a href={url}>Click</a>; // javascript:alert(1) works here!
}
Corrección: Valida las URLs con un helper:
function isValidUrl(url: string): boolean {
try {
const parsed = new URL(url, window.location.origin);
return parsed.protocol === "http:" || parsed.protocol === "https:";
} catch {
return false;
}
}
function UserLink({ url }: { url: string }) {
if (!isValidUrl(url)) return null;
return <a href={url}>Click</a>;
}
2. Cabeceras CSP para aplicaciones React
La Política de Seguridad de Contenido (Content Security Policy, CSP) es tu segunda línea de defensa contra el XSS. Le dice al navegador qué fuentes de contenido son de confianza.
Configuración de CSP
Para una aplicación Next.js, configura la CSP vía middleware o next.config.ts:
// next.config.ts
const csp = `
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' https: data:;
connect-src 'self' https://api.yoursite.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
`.replace(/\s{2,}/g, " ").trim();
module.exports = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "Content-Security-Policy", value: csp },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
],
},
];
},
};
Para Create React App, usa el enfoque de CRA sirviendo el HTML con las cabeceras configuradas en el servidor (nginx, Apache o una CDN como Cloudflare):
# nginx.conf
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
Probando tu CSP
Usa el CSP Evaluator de Google para comprobar si hay debilidades en la política. Una política con 'unsafe-inline' en script-src no ofrece ninguna protección contra la inyección de scripts — considera mover los scripts inline a archivos separados o usar una política basada en nonce/hash:
// Nonce-based CSP for Next.js
const nonce = crypto.randomBytes(16).toString("base64");
const csp = `default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}';`;
3. Seguridad de dependencias
Tu aplicación solo es tan segura como su dependencia más débil. Las aplicaciones React modernas incorporan fácilmente más de 1.000 paquetes a través del árbol de dependencias.
npm audit y sus limitaciones
npm audit señala vulnerabilidades conocidas contra la base de datos de avisos de npm:
npm audit
Pero produce falsos positivos (vulnerabilidades señaladas en dependencias solo de desarrollo) y falsos negativos (vulnerabilidades aún no divulgadas o publicadas). Es una línea base, no un sustituto.
Casos de estudio de ataques a la cadena de suministro
- event-stream (2018): Se inyectó un paquete malicioso,
flatmap-stream, en el popular paquete npmevent-stream, dirigido a una aplicación específica de carteras de Bitcoin para robar claves privadas. - ua-parser-js (2021): Se comprometieron las credenciales npm del mantenedor y se publicó código malicioso que instalaba mineros de criptomonedas.
- node-ipc (2022): Protestware que borraba archivos en ciertas regiones.
Estrategia de defensa
# Audit every CI run
npm audit --audit-level=high
# Use exact versions for critical deps
# package.json: "react": "19.2.6" (no ^ or ~)
# Verify lockfile integrity
npm audit signatures
Para un análisis más profundo, usa Snyk o Socket.dev, que van más allá de los CVE y analizan el comportamiento de los paquetes (por ejemplo, llamadas de red, acceso al sistema de archivos, código ofuscado):
npx snyk test
Revisa siempre los cambios del lockfile en los PR. Un package-lock.json o yarn.lock modificado con un salto de versión sospechoso en una subdependencia es una señal de alarma.
4. OWASP Top 10 para React
A01: Control de acceso roto
Las aplicaciones React exponen rutas de API que deben aplicar la autorización en el servidor. Nunca confíes solo en ocultar elementos de la interfaz:
// VULNERABLE: only hides the admin panel
function AdminPanel() {
const { user } = useAuth();
if (user.role !== "admin") return null; // Client-side only!
return <AdminDashboard />;
}
Corrección: Verifica la autorización en el handler de tu ruta de API o server action:
// server-side check (Next.js Route Handler)
export async function GET(req: Request) {
const session = await getSession(req);
if (session?.role !== "admin") {
return Response.json({ error: "Forbidden" }, { status: 403 });
}
return Response.json(await getSensitiveData());
}
A03: Inyección (XSS)
Cubierto en la Sección 1. Para endpoints GraphQL/REST, valida y sanitiza también todas las entradas antes de que lleguen a tu base de datos. Usa Zod o Valibot para la validación de esquemas:
import { z } from "zod";
const commentSchema = z.object({
body: z.string().max(5000),
postId: z.string().uuid(),
});
A05: Configuración de seguridad incorrecta
Las cabeceras CSP ausentes, los archivos .env expuestos y los orígenes CORS innecesarios son habituales. Audita tus cabeceras de respuesta con:
curl -sI https://yoursite.com | grep -iE "content-security-policy|x-frame-options|strict-transport-security"
A06: Componentes vulnerables y desactualizados
Usa npm outdated con regularidad:
npm outdated
Configura Dependabot o Renovate para automatizar las actualizaciones menores y de parche. Para actualizaciones mayores, audita manualmente los cambios que rompen la compatibilidad.
A08: Fallos de integridad del software y de los datos
La Integridad de Subrecursos (Subresource Integrity, SRI) para scripts externos, la verificación del lockfile y los commits firmados protegen la integridad de tu cadena de suministro:
<script src="https://cdn.example.com/lib.js" integrity="sha384-abc123..." crossorigin="anonymous"></script>
5. Autenticación y autorización
Almacenamiento de JWT
Nunca almacenes JWT en localStorage — es accesible para cualquier JavaScript del mismo origen, lo que lo hace trivialmente robable vía XSS. Usa cookies httpOnly, Secure y SameSite=Strict:
// Server-side (Next.js Route Handler)
export async function POST(req: Request) {
const { token } = await req.json();
// Set as httpOnly cookie
const headers = new Headers();
headers.append(
"Set-Cookie",
`session=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400`
);
return Response.json({ ok: true }, { headers });
}
Flujos OAuth
Para OAuth (Google, GitHub, etc.), usa el flujo de Código de Autorización con PKCE, nunca el flujo implícito (Implicit Flow):
// PKCE code verifier generation
const generateCodeVerifier = () => {
const array = crypto.getRandomValues(new Uint8Array(32));
return btoa(String.fromCharCode(...array))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
};
Protección CSRF
Si usas autenticación basada en cookies, los tokens CSRF son obligatorios a menos que configures SameSite=Strict. Para rutas de API, usa un patrón de cookie de doble envío (double-submit cookie) o un token CSRF dedicado:
// Next.js middleware CSRF check
import { csrf } from "@/lib/csrf";
export async function middleware(req: NextRequest) {
if (req.method !== "GET") {
await csrf(req); // Throws 403 if invalid
}
return NextResponse.next();
}
6. Lista de verificación de auditoría de seguridad de React
Usa esta lista en cada ciclo de auditoría:
- [ ] Cabeceras CSP configuradas y probadas con CSP Evaluator
- [ ] Uso de
dangerouslySetInnerHTMLrevisado — ¿sanitizado con DOMPurify? - [ ] Props
href/srcvalidadas frente ajavascript:y otros esquemas peligrosos - [ ] Todas las dependencias actualizadas (
npm outdated,npm audit) - [ ] Lockfile revisado por cambios inesperados en subdependencias
- [ ] Cadena de suministro: escaneo de Snyk/Socket.dev configurado en CI
- [ ] JWT almacenado en cookies
httpOnly(nunca enlocalStorage) - [ ] Protección CSRF implementada para peticiones que cambian estado
- [ ] OWASP Top 10 probado — especialmente A01, A03, A05, A06, A08
- [ ] Variables de entorno eliminadas del bundle de cliente (sin
NEXT_PUBLIC_*para secretos) - [ ] Endpoints de API con limitación de velocidad (rate limiting) y verificación de autorización en el servidor
- [ ] Hashes SRI presentes en scripts de CDN externos
- [ ] Cabeceras de seguridad presentes:
X-Content-Type-Options,X-Frame-Options,Referrer-Policy
Conclusión
La seguridad no es un elemento de una lista de verificación de una sola vez — es una práctica continua que debe ir al ritmo de la evolución de tu aplicación. Cada nueva dependencia, cada cambio de funcionalidad y cada despliegue es una oportunidad para que se cuele una vulnerabilidad. Integra el escaneo de seguridad automatizado en tu pipeline de CI, revisa tu CSP trimestralmente y haz que las revisiones de seguridad formen parte de tu flujo de trabajo de pull requests. Si necesitas una evaluación exhaustiva y profesional de la postura de seguridad de tu aplicación React, contáctanos para una auditoría de seguridad completa. Nuestro equipo mapea toda tu superficie de ataque — desde los árboles de dependencias hasta las cabeceras de despliegue — y entrega un plan de remediación priorizado.
JS Security Audit
Auditorías dirigidas por un ingeniero de seguridad JavaScript senior con más de 10 años de experiencia.