<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Buenas prácticas combinadas para apps Web modernas: Seguridad, cache y Websocket

Las mejores prácticas individuales de seguridad y rendimiento web están ampliamente documentadas, pero rara vez se explican juntos como un sistema coherente: TLS en todas partes, cookies de sesión configurado correctamente, sin JWT en localStorage, almacenamiento en caché agresivo solo donde sea seguro hacerlo, Revocación real de sesiones y WebSockets, limitación de tarifas en canales en tiempo real y monitorización de conexiones todas estas señales. Esta guía cubre diez prácticas que, aplicadas en conjunto, realmente reducen la superficie de ataque sin sacrificar el rendimiento percibido por los usuarios.

Cada sección incluye configuraciones concretas (Nginx, Express, NestJS, WebSocket) y las compensaciones reales. de cada elección, porque cada práctica tiene un costo, y aplicarlas sin comprenderlas conduce a configuraciones culto a la carga que realmente no protege nada.

1. Se requiere HTTPS/WSS

TLS no es negociable ni para HTTP ni para WebSocket: sin wss://, un WebSocket de texto sin formato expone tokens de sesión y cargas útiles de aplicaciones a cualquier persona en la misma red. Configurar TLS 1.2 como mínimo (1.3 cuando sea posible), HSTS con preload y grapado OCSP para evitar la latencia una verificación del certificado en tiempo real en cada apretón de manos.

# nginx.conf — TLS 1.2/1.3, HSTS, OCSP stapling
server {
  listen 443 ssl;
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_stapling on;
  ssl_stapling_verify on;
  add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

  location /ws/ {
    proxy_pass http://backend_ws;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  }
}

2. Cookies de sesión: HttpOnly, seguras, SameSite

HttpOnly evita que JavaScript lea la cookie (mitiga el robo a través de XSS), Secure solo lo envía a través de HTTPS, SameSite controla el envío entre sitios: Estricto para máxima protección CSRF (pero interrumpe los flujos con redirecciones externas), Lax como un compromiso razonable para la mayoría de las aplicaciones, Ninguno solo si la cookie debe ser verdaderamente entre sitios, y en ese caso requiere Seguro obligatorio.

// Express/NestJS — cookie di sessione configurato correttamente
res.cookie('session_id', sessionToken, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  maxAge: 15 * 60 * 1000, // 15 minuti, coerente con la strategia di refresh
});

3. Nunca JWT en localStorage

localStorage es legible por cualquier script ejecutado en la página: un único XSS (incluso en una dependencia de terceros, no en su código) exfiltra el token sin necesidad de ninguna interacción usuario. La alternativa segura es la HttpOnly cookie para el token en sí, combinada con el patrón cookie de doble envío para protección CSRF cuando también necesita una mecanismo sin estado.

// Refresh token in HttpOnly cookie, access token short-lived in memoria (mai in storage persistente)
app.post('/auth/refresh', (req, res) => {
  const refreshToken = req.cookies.refresh_token; // HttpOnly, non leggibile da JS
  const newAccessToken = issueAccessToken(verifyRefreshToken(refreshToken));
  res.json({ accessToken: newAccessToken }); // vive solo in memoria lato client, mai in localStorage
});

4. Caché de activos estáticos con huellas dactilares

Un activo con hash en el nombre de archivo (main.a1b2c3d4.js) se puede almacenar en caché durante un año completo con inmutable, porque cada cambio en el contenido genera automáticamente un nombre de archivo diferente: invalidar el caché nunca requiere una purga manual.

# Angular CLI genera automaticamente asset con hash nel filename in build di produzione
ng build --configuration production
# Output: main.a1b2c3d4.js, styles.e5f6a7b8.css
# Header cache per asset fingerprinted
location ~* \.[0-9a-f]{8}\.(js|css)$ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}

5. No almacene en caché respuestas confidenciales

Cualquier respuesta API con datos personales o de sesión del usuario debe indicar explícitamente Cache-Control: no-store (nunca guardado, ni siquiera en la memoria temporal) o privado (almacenamiento en caché solo desde el navegador del usuario, nunca desde un caché/CDN compartido). Si la respuesta varía en basado en un encabezado como Autorización o Accept-Language, declararlo con Var para evitar que un caché intermedio entregue una respuesta de usuario incorrecta.

// NestJS — risposta con dati personali, mai cacheabile
@Get('me')
getProfile(@Res({ passthrough: true }) res: Response) {
  res.setHeader('Cache-Control', 'no-store');
  return this.userService.getCurrentProfile();
}

6. CDN con s-maxage Separado del navegador

s-maxage controla cuánto tiempo la CDN (caché compartida) mantiene la respuesta, independientemente de que max-age controle el navegador del usuario, esto permite proporcione contenido cuasi estático desde el borde durante minutos mientras mantiene su navegador actualizado en minutos segundos, o viceversa.

# Header: CDN cachea 5 minuti, browser solo 30 secondi
Cache-Control: public, s-maxage=300, max-age=30

# Purge mirata di un singolo path sulla CDN dopo un deploy
curl -X POST "https://api.cdn-provider.com/purge" -H "Authorization: Bearer $CDN_TOKEN" -d '{"path":"/api/products"}'

7. Actualizar y revocar tokens para sesiones y WebSockets

Un token de acceso de corta duración (10-15 minutos) más un token de actualización HttpOnly de mayor duración limitan la Daños en la ventana en caso de robo. La revocación real requiere el estado del lado del servidor (una lista negra o un contador de versiones por usuario): un JWT puramente sin estado no puede ser invalidada antes de su vencimiento natural. Para WebSockets, la revocación debe cerrar activamente el conexión, no rechace simplemente las solicitudes HTTP posteriores.

// Revoca sessione: invalida il refresh token e chiude ogni WebSocket associato all'utente
async function revokeSession(userId: string) {
  await sessionStore.incrementTokenVersion(userId); // invalida tutti i JWT emessi prima di ora
  for (const socket of wsConnections.getByUserId(userId)) {
    socket.close(4001, 'session_revoked');
  }
}

8. Limitación de velocidad y límite de tamaño en WebSocket

Sin límites, un solo cliente puede saturar el servidor con mensajes o cargas útiles de alta velocidad. enorme: es la forma más sencilla de aplicación DoS para ejecutar en un canal que no sea WebSocket protegido.

// ws — rate limiting e size limit per connessione
const wss = new WebSocketServer({ maxPayload: 64 * 1024 }); // 64KB max per messaggio

wss.on('connection', (socket) => {
  let messageCount = 0;
  const resetInterval = setInterval(() => (messageCount = 0), 1000);

  socket.on('message', (data) => {
    if (++messageCount > 20) return socket.close(4008, 'rate_limit_exceeded'); // max 20 msg/sec
    handleMessage(data);
  });

  socket.on('close', () => clearInterval(resetInterval));
});

9. Monitoreo

Las métricas a seguir cubren cuatro áreas: conexiones (WebSockets activos, tasa reconexión), latencia/rendimiento (tiempo de respuesta de API, mensajes/segundo), cache (proporción de aciertos/errores para la CDN, TTL efectivo observado) y auth (tasa de error del token de actualización, sesiones revocadas). Prometheus + Grafana cubren bien las métricas paneles y numéricos en tiempo real; ELK (o equivalente) sigue siendo el más adecuado para el registro de aplicaciones Investigación estructurada y ad hoc sobre eventos de seguridad.

// Prometheus — contatore custom per connessioni WebSocket attive
const wsConnectionsGauge = new client.Gauge({ name: 'ws_active_connections', help: 'Connessioni WebSocket attive' });
wss.on('connection', () => {
  wsConnectionsGauge.inc();
  return () => wsConnectionsGauge.dec();
});

10. Prueba de accesibilidad y privacidad

Verifique que ninguna cookie o encabezado almacenado en caché exponga inadvertidamente datos personales (un Set-Cookie con correo electrónico en texto sin formato en el nombre, una respuesta almacenada públicamente en caché que contiene el nombre de usuario). Pruebas automatizadas (axe-core para a11y, escáner de encabezado de seguridad IC) cubren casos objetivos; una revisión manual periódica de los encabezados Cache-Control en Las rutas con datos personales siguen siendo necesarias porque un nuevo punto final puede olvidar fácilmente el encabezado correcto.

Lista de verificación operativa rápida

  • Alta prioridad: HTTPS/WSS en todas partes, HttpOnly+Secure+SameSite cookie de sesión, elimina cualquier JWT del almacenamiento local.
  • Prioridad media: toma de huellas dactilares de activos estáticos, no-store encabezado en respuestas con datos personales, WebSocket que limita la velocidad.
  • Prioridad continua: monitoreo de conexión/caché/autenticación, revisión periódica de encabezados de caché en nuevas rutas.

Plan de 30/60/90 días

Días 1-30

  • HTTPS/WSS forzado en todas partes, HSTS activo — KPI: 0 puntos finales accesibles en texto plano.
  • Las cookies de sesión se migraron a HttpOnly+Secure+SameSite — KPI: 0 tokens leídos desde JavaScript del lado del cliente.

Días 31-60

  • Limitación de velocidad activa en todos los canales WebSocket — KPI: 0 conexiones capaces de exceder los límites establecidos.
  • Se corrigieron encabezados de almacenamiento en caché en todas las rutas con datos personales: KPI: auditoría completa, 0 respuestas confidenciales almacenables en caché públicamente.

Días 61-90

  • Panel de monitoreo completo (conexiones, caché, autenticación) — KPI: alertas configuradas en cada métrica crítica.
  • Revocación de tokens y WebSocket probada de extremo a extremo — KPI: tiempo de revocación efectivo inferior a 2 segundos.

Preguntas frecuentes

SameSite=¿Estricta o laxa para una cookie de sesión?

Lax es el compromiso correcto para la mayoría de las aplicaciones; Estricto solo si no tienes flujos con redireccionamientos desde dominios externos a la app.

¿Por qué no usar sessionStorage en lugar de localStorage para JWT?

Ambos son igualmente legibles mediante JavaScript y, por lo tanto, vulnerables a XSS: la única diferencia es la durabilidad, no la seguridad.

¿maxage funciona sin una CDN configurada?

No, los navegadores lo ignoran: solo afecta a las cachés compartidas compatibles, como CDN o proxies inversos que lo admiten explícitamente.

¿Cómo revoco un JWT antes de que caduque?

Con estado del lado del servidor (lista negra o versión de token por usuario), porque un JWT puro sin estado no se puede invalidar antes del vencimiento natural.

¿Cuál es un límite razonable de mensajes por segundo para un WebSocket?

Depende del caso de uso, pero entre 10 y 30 mensajes/segundo por conexión es un punto de partida razonable para la mayoría de las aplicaciones en tiempo real.

¿Es obligatorio el grapado OCSP?

No es obligatorio, pero se recomienda encarecidamente: reduce la latencia del protocolo de enlace TLS al evitar que el cliente se comunique con la autoridad de certificación por separado.

¿Cómo se puede evitar que una CDN entregue una respuesta confidencial al cliente equivocado?

Con Cache-Control: privado o no-store en respuestas personales, nunca público cuando los datos varían según el usuario.

¿También necesita monitorear las fallas de actualización de tokens?

Sí, un aumento repentino en las fallas de actualización es a menudo la primera señal de un ataque en curso o un error de vencimiento del token.

Errores comunes que se deben evitar

  • WebSocket de texto claro (ws://) detrás de una interfaz HTTPS: el navegador solo permite esto si el WebSocket está en el mismo origen inseguro, pero aún expone datos de texto sin formato a la red.
  • Las cookies sin Secure en desarrollo también quedan en producción: a menudo copiadas de una configuración de prueba que nunca se actualizó.
  • JWT en localStorage "temporalmente" durante el desarrollo: casi siempre se vuelve permanente porque "funciona" y nadie lo vuelve a ver.
  • Control de caché ausente de forma predeterminada en nuevas rutas API: sin un encabezado explícito, el comportamiento del almacenamiento en caché depende del cliente y no está garantizado.
  • s-maxage y max-age idénticos: elimina la ventaja de poder invalidar el caché del navegador más rápidamente que el caché perimetral.
  • No hay tamaño máximo en los mensajes de WebSocket: un solo cliente puede enviar enormes cargas útiles y saturar la memoria del servidor.
  • Revocar que solo bloquea futuras solicitudes HTTP: dejar activas las conexiones WebSocket existentes con el mismo token comprometido.
  • No hay monitoreo de aciertos/errores de caché: una proporción de aciertos decreciente a menudo indica una regresión en los encabezados de caché que ha pasado desapercibida.

Respuestas rápidas

¿Por qué también se requiere HTTPS para WebSockets? Un WebSocket de texto sin formato (ws://) expone tokens de sesión y cargas útiles de aplicaciones a cualquier persona en la misma red; wss:// cifra toda la conexión exactamente como lo hace HTTPS para las solicitudes HTTP tradicionales.

¿Cuál es el indicador SameSite de una cookie? Controla si una cookie se envía en solicitudes entre sitios: Strict bloquea el envío entre sitios, Lax lo permite solo para navegación de nivel superior, Ninguno siempre permite pero requiere Secure.

¿Por qué no guardar un JWT en el almacenamiento local? Porque es legible por cualquier script JavaScript que se ejecute en la página: un único XSS, incluso en una biblioteca de terceros, permite que el token se extraiga sin ninguna interacción del usuario.

¿Qué significa Cache-Control: inmutable? Le dice al navegador que el contenido de esa URL nunca cambiará mientras la URL siga siendo la misma, evitando incluso una solicitud de validación condicional durante el período de caché.

¿Cómo se revoca una sesión con WebSockets activos? Invalidando el estado del lado del servidor del token (lista negra o versión) y cerrando activamente todas las conexiones WebSocket asociadas con ese usuario, no solo rechazando nuevas solicitudes HTTP.

¿Cuál es la diferencia entre max-age y s-maxage? max-age verifica el caché del navegador del usuario, s-maxage verifica los cachés compartidos como CDN y proxy inverso, y cuando está presente tiene prioridad sobre edad máxima para esos cachés.

Cómo comprobar

  • Compruebe las redirecciones TLS y HTTPS: curl -I https://your-domain.com y verifique el encabezado Strict-Transport-Security.
  • Inspeccionar el certificado y protocolo TLS: openssl s_client -connect your-domain.com:443 -tls1_2.
  • Compruebe los encabezados de caché en los activos: curl -I https://your-domain.com/main.a1b2c3d4.js, marque immutable y edad máxima.
  • Compruebe que las respuestas confidenciales no se puedan almacenar en caché: curl -I https://your-domain.com/api/me, marque no-store.
  • Simular carga en WebSocket para validar la limitación de velocidad: npx artillería rápida --count 100 -n 20 wss://your-domain.com/ws.
  • Pruebe la revocación de un extremo a otro: revoque una sesión a través de API y verifique que el WebSocket asociado se cierre en unos segundos.

Conclusión

Ninguna de estas diez prácticas es suficiente por sí sola: TLS sin cookies correctamente configuradas todavía deja la sesión vulnerable, almacenamiento en caché agresivo sin distinguir las respuestas públicas de privado expone datos personales, y la limitación de tarifas sin monitoreo no le permite saber si es realmente funcionando. Aplicados en conjunto, con un plan de adopción priorizado (HTTPS y cookies primero, almacenamiento en caché y limitación de velocidad (luego, monitoreo continuo siempre), forman una base de seguridad y rendimiento consistente para cualquier aplicación web moderna con componentes en tiempo real.

¿Quiere una lista de verificación imprimible o una evaluación de su configuración de seguridad? aplicación? Solicitar una auditoría técnica: en pocas horas de análisis es posible identificar el brechas prioritarias en cookies, almacenamiento en caché, WebSockets y monitoreo.

💬 Notas de los lectores

0 notas

Escribe una nota

Comparte tu opinión, una sugerencia o un cumplido

Últimas notas

Aún no hay notas. ¡Sé el primero en comentar!