OWASP en el frontend
Las mismas diez categorías del Top 10, pero el frontend falla en sitios distintos que el backend: casi todo se concentra en XSS, en dónde guardas la sesión y en lo que se te escapa dentro del bundle.
Para la parte de servidor —queries, autorización de servicio, SSRF entre servicios— usa
/backend:owasp-security. Esta skill cubre el navegador y la capa de servidor que vive
dentro del framework de frontend (Server Actions, route handlers, middleware).
La regla que ordena todo lo demás
El código que corre en el navegador es público y está bajo control del usuario. No es una
opinión: cualquiera abre las devtools, edita el bundle, quita un if y repite la petición con
curl. De ahí salen las tres reglas que no se negocian:
- Toda validación del cliente es usabilidad, no seguridad. Se duplica en servidor. Siempre.
- Todo secreto que llegue al bundle está publicado. No hay forma de ofuscarlo.
- Ocultar la UI no protege el endpoint. Si el botón de admin está escondido pero la ruta responde, no hay control de acceso.
Mapa rápido
| Lo que estás tocando | Dónde suele romperse |
|---|---|
| Renderizar contenido que viene del usuario o de una API | XSS (§1) |
| Login, sesión, refresh de token | Almacenamiento de sesión (§2), CSRF (§4) |
| Variables de entorno, claves de API, feature flags | Secretos en el bundle (§3) |
| Formulario que muta estado, Server Action | CSRF (§4), autorización (§5) |
Route handler, middleware, next/image, proxy |
Autorización (§5), SSRF (§7) |
package.json, un paquete nuevo, un script de terceros |
Cadena de suministro (§6) |
| Salir a producción | Cabeceras y CSP (§8), checklist final |
1. XSS — el riesgo número uno del frontend
React escapa por defecto todo lo que interpolas en JSX. El XSS entra por las puertas que se saltan ese escape, y son pocas y conocidas:
// 1. dangerouslySetInnerHTML — el nombre avisa
<div dangerouslySetInnerHTML={{ __html: comentario }} />
// 2. href/src con esquema controlado por el usuario
<a href={urlDelUsuario}>…</a> // javascript:alert(1) ejecuta al hacer clic
// 3. Escapar de React: refs + innerHTML, document.write, eval, new Function
ref.current.innerHTML = contenido;
Arreglos, en orden de preferencia:
- No renderizar HTML. Si el contenido es texto, trátalo como texto. La mayoría de los
dangerouslySetInnerHTMLque hay en producción no necesitaban serlo. - Si necesitas HTML enriquecido, sanitiza con una librería mantenida (DOMPurify) y hazlo al guardar, en servidor, no solo al pintar: así un consumidor distinto del mismo dato no repite el agujero.
- Valida el esquema de las URLs antes de ponerlas en
hrefosrc:
const seguro = (u) => {
try { return ['http:', 'https:', 'mailto:'].includes(new URL(u, location.origin).protocol) }
catch { return false }
}
- Markdown no es seguro por defecto. Casi todos los renderizadores permiten HTML embebido salvo que lo desactives explícitamente.
- Una CSP es la red de seguridad, no el arreglo (§8). Reduce el daño de un XSS que se coló; no evita que exista.
Y ojo con el XSS que entra por datos, no por marcado: un objeto de configuración, un nombre de
tema o una traducción que vienen de la API y terminan concatenados en un style o en un
atributo.
2. Dónde guardar la sesión
La pregunta más común y la que más se responde mal.
Cookie httpOnly |
localStorage |
|
|---|---|---|
| ¿La lee un XSS? | No | Sí, en una línea |
| ¿Se envía sola? | Sí → necesita defensa CSRF (§4) | No |
| ¿Sobrevive a recarga? | Sí | Sí |
| Recomendado para | el token de sesión | nada sensible |
Cookie httpOnly + Secure + SameSite=Lax (o Strict) para el token de sesión. El
argumento de "localStorage evita CSRF" es cierto pero cambia un riesgo por otro peor: CSRF
tiene defensas mecánicas y baratas; un XSS con el token en localStorage es una toma de cuenta
completa y silenciosa.
Access token corto y refresh token revocable en servidor. Y no guardes en el cliente datos que el usuario no debería poder leer: si está en el navegador, es suyo.
3. Secretos en el bundle
NEXT_PUBLIC_* → va al navegador. Cualquier valor ahí es público.
El error clásico es poner una clave de servicio en una variable NEXT_PUBLIC_ "porque la
necesitaba un componente cliente". Si un componente cliente la necesita, el diseño está mal:
la llamada tiene que pasar por una route handler que guarde la clave en servidor.
Antes de desplegar, búscalo en el bundle compilado, no en el código:
npm run build && grep -rIE '(sk-|AKIA|-----BEGIN|eyJhbGciOi)' .next/static/ | head
Y desactiva los source maps públicos en producción salvo que los subas a tu herramienta de errores de forma privada: un source map público devuelve tu código sin minificar, comentarios incluidos.
4. CSRF y clickjacking
CSRF. Aplica a todo lo que mute estado y se autentique con cookie. Las defensas, de menos a más trabajo:
SameSite=Laxen la cookie de sesión — cubre la mayoría de los casos y es una línea.- Token anti-CSRF por formulario si necesitas
SameSite=None(flujos entre dominios). - Comprobar
Originen el servidor en las peticiones que mutan.
En Next.js las Server Actions traen protección CSRF de origen, pero eso no las autoriza: sigue siendo tu trabajo comprobar quién es el usuario dentro de la acción (§5).
Clickjacking. X-Frame-Options: DENY, o mejor frame-ancestors 'none' en la CSP. Si tu app
se embebe legítimamente, enumera los orígenes en vez de abrirlo.
5. Autorización: el frontend no autoriza
Ocultar el botón está bien para la experiencia y no vale nada como control.
- Cada Server Action y cada route handler comprueba sesión y permiso, aunque solo se invoque desde una pantalla ya protegida. Una Server Action es un endpoint HTTP público con otro nombre.
- El middleware es un filtro, no la autorización. Sirve para redirigir a login; no sustituye la comprobación dentro del handler, porque no todas las rutas pasan por él y su matcher se desactualiza.
- No confíes en un id que venga del cliente. El usuario sale del token en servidor, y el recurso se filtra por dueño dentro de la consulta.
- Cuidado con lo que cruza de servidor a cliente. Todo lo que le pasas como prop a un componente cliente viaja serializado al navegador: si le pasas el objeto de usuario entero, el hash de la contraseña y el correo interno van dentro. Selecciona los campos.
6. Dependencias y terceros
El frontend tiene la cadena de suministro más expuesta que existe: cientos de paquetes transitivos que ejecutan scripts en tu build y código en el navegador de tus usuarios.
npm audit --omit=deven CI, y una revisión periódica que alguien firme.- Lockfile commiteado y
npm cien CI, nuncanpm install. - Antes de añadir un paquete: descargas, fecha del último commit, cuántas dependencias transitivas arrastra. Para algo trivial, escríbelo.
- Los scripts de terceros (analítica, chat, píxeles) corren con todos tus privilegios en la
página. Cárgalos con
next/scripty la estrategia correcta, limita cuáles permite tu CSP, y usa SRI (integrity) cuando el proveedor publique el hash. - Un
<iframe>de terceros va consandboxy los permisos mínimos.
7. SSRF y proxies del frontend
Aparece en cuanto tu route handler pide una URL que decide el cliente: previsualizar un enlace, importar desde una URL, un avatar remoto, un proxy de imágenes.
Desde el servidor esa URL alcanza la red interna y el metadata service del proveedor cloud.
Valida antes de pedir: solo http/https, resuelve el DNS y rechaza IPs privadas o de
loopback, no sigas redirecciones automáticamente, y pon timeout y límite de tamaño.
Caso concreto de Next.js: images.remotePatterns con ** convierte el optimizador de imágenes
en un proxy abierto. Enumera los dominios.
8. Cabeceras y CSP
Mínimo para producción:
Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'
Strict-Transport-Security: max-age=63072000; includeSubDomains
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Dos avisos sobre la CSP que ahorran una tarde:
unsafe-inlineenscript-srcanula la mitad del beneficio. Si el framework necesita scripts inline, usa nonces por petición en vez de abrirlo.- Despliégala primero en
Content-Security-Policy-Report-Onlyy mira los informes unos días. Una CSP estricta activada de golpe rompe la app en producción, y la reacción típica es desactivarla entera.
CORS es del servidor, no del navegador: Access-Control-Allow-Origin: * junto con credenciales
es inválido además de peligroso. Enumera los orígenes.
Checklist antes de salir a producción
- Cero
dangerouslySetInnerHTMLsin sanitizar; los que queden, justificados y con la sanitización en el punto de guardado - Esquema validado en toda
href/srcque venga de datos - Token de sesión en cookie
httpOnly+Secure+SameSite; nada sensible enlocalStorage -
grepde secretos sobre el bundle compilado, no sobre el código - Source maps de producción no públicos
- Cada Server Action y route handler comprueba sesión y permiso por su cuenta
- Los props que cruzan a componentes cliente llevan solo los campos necesarios
- Validación de cliente duplicada en servidor, sin excepciones
- CSP desplegada (probada antes en Report-Only) + HSTS +
nosniff+frame-ancestors -
npm cicon lockfile;npm auditen CI; scripts de terceros con SRI y en la lista de la CSP -
images.remotePatternsy cualquier proxy con dominios enumerados - Errores hacia el usuario sin stack trace ni detalle de infraestructura
Referencia
Categorías según el OWASP Top 10:2021. Para el detalle por tema, las OWASP Cheat Sheets — en particular XSS Prevention, Content Security Policy y CSRF Prevention.