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

Best Practice combinate per applicazioni Web moderne: Sicurezza, cache e Websocket

Le singole best practice di sicurezza e performance web sono ampiamente documentate, ma raramente vengono spiegate insieme come un sistema coerente: TLS ovunque, cookie di sessione configurati correttamente, niente JWT in localStorage, caching aggressivo solo dove è sicuro farlo, revoca reale di sessioni e WebSocket, rate limiting sui canali real-time e monitoring che collega tutti questi segnali. Questa guida copre le dieci pratiche che, applicate insieme, riducono davvero la superficie di attacco senza sacrificare le performance percepite dagli utenti.

Ogni sezione include configurazioni concrete (Nginx, Express, NestJS, WebSocket) e i trade-off reali di ogni scelta — perché ogni pratica ha un costo, e applicarle senza capirlo porta a configurazioni cargo-cult che non proteggono davvero nulla.

1. HTTPS / WSS Obbligatorio

TLS non è negoziabile né per HTTP né per WebSocket: senza wss://, un WebSocket in chiaro espone token di sessione e payload applicativi a chiunque sulla stessa rete. Configura TLS 1.2 come minimo (1.3 dove possibile), HSTS con preload, e OCSP stapling per evitare la latenza di una verifica del certificato in tempo reale ad ogni handshake.

# 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. Cookie di Sessione: HttpOnly, Secure, SameSite

HttpOnly impedisce a JavaScript di leggere il cookie (mitiga il furto via XSS), Secure lo invia solo su HTTPS, SameSite controlla l'invio cross-site: Strict per massima protezione CSRF (ma rompe flussi con redirect esterni), Lax come compromesso ragionevole per la maggior parte delle applicazioni, None solo se il cookie deve essere davvero cross-site — e in quel caso richiede Secure obbligatoriamente.

// 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. Mai JWT in localStorage

localStorage è leggibile da qualsiasi script eseguito sulla pagina: un singolo XSS (anche in una dipendenza terza, non nel tuo codice) esfiltra il token senza bisogno di alcuna interazione utente. L'alternativa sicura è l'HttpOnly cookie per il token stesso, combinato con il pattern double submit cookie per la protezione CSRF quando serve anche un meccanismo stateless.

// 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. Cache degli Static Asset con Fingerprinting

Un asset con hash nel filename (main.a1b2c3d4.js) può essere cacheato per un anno intero con immutable, perché ogni modifica al contenuto genera automaticamente un nome file diverso — invalidare la cache non richiede mai una purge manuale.

# 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. Non Cacheare Risposte Sensibili

Ogni risposta API con dati personali o legati alla sessione utente deve dichiarare esplicitamente Cache-Control: no-store (mai salvata, nemmeno in memoria temporanea) o private (cacheabile solo dal browser dell'utente, mai da una cache condivisa/CDN). Se la risposta varia in base a header come Authorization o Accept-Language, dichiaralo con Vary per evitare che una cache intermedia serva la risposta sbagliata utente.

// 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 Separato dal Browser

s-maxage controlla per quanto tempo la CDN (cache condivisa) mantiene la risposta, indipendentemente da max-age che controlla il browser dell'utente — questo permette di servire contenuto quasi-statico dall'edge per minuti mantenendo il browser sempre aggiornato in pochi secondi, 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. Token Refresh e Revoca per Sessioni e WebSocket

Un access token a vita breve (10-15 minuti) più un refresh token HttpOnly a vita più lunga limita la finestra di danno in caso di furto. La revoca reale richiede uno stato lato server (una blacklist o un contatore di versione per utente): un JWT puramente stateless non può essere invalidato prima della sua scadenza naturale. Per i WebSocket, la revoca deve chiudere attivamente la connessione, non limitarsi a rifiutare le richieste HTTP successive.

// 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. Rate Limiting e Size Limit su WebSocket

Senza limiti, un singolo client può saturare il server con messaggi ad alta frequenza o payload enormi — è la forma di DoS applicativo più semplice da eseguire contro un canale WebSocket non protetto.

// 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. Monitoring

Le metriche da tracciare coprono quattro aree: connessioni (WebSocket attive, tasso di riconnessione), latenza/throughput (tempo di risposta API, messaggi/secondo), cache (hit/miss ratio per la CDN, TTL effettivo osservato) e auth (tasso di fallimento del refresh token, sessioni revocate). Prometheus + Grafana coprono bene metriche numeriche e dashboard in tempo reale; ELK (o un equivalente) resta più adatto per il log applicativo strutturato e la ricerca ad hoc su eventi di sicurezza.

// 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. Test di Accessibilità e Privacy

Verifica che nessun cookie o header cacheato esponga inavvertitamente dati personali (un Set-Cookie con l'email in chiaro nel nome, una risposta cacheata pubblicamente che contiene il nome utente). I test automatici (axe-core per a11y, scanner di header di sicurezza in CI) coprono i casi oggettivi; una revisione manuale periodica dei header Cache-Control sulle rotte con dati personali resta necessaria perché un nuovo endpoint può facilmente dimenticare l'header corretto.

Checklist Operativa Rapida

  • Priorità alta: HTTPS/WSS ovunque, cookie di sessione HttpOnly+Secure+SameSite, rimozione di ogni JWT da localStorage.
  • Priorità media: fingerprinting asset statici, header no-store su risposte con dati personali, rate limiting WebSocket.
  • Priorità continua: monitoring di connessioni/cache/auth, revisione periodica degli header di cache su nuove rotte.

Piano 30/60/90 Giorni

Giorni 1-30

  • HTTPS/WSS forzato ovunque, HSTS attivo — KPI: 0 endpoint raggiungibili in chiaro.
  • Cookie di sessione migrati a HttpOnly+Secure+SameSite — KPI: 0 token letti da JavaScript client-side.

Giorni 31-60

  • Rate limiting attivo su tutti i canali WebSocket — KPI: 0 connessioni in grado di superare i limiti impostati.
  • Header di cache corretti su tutte le rotte con dati personali — KPI: audit completo, 0 risposte sensibili cacheabili pubblicamente.

Giorni 61-90

  • Dashboard di monitoring completa (connessioni, cache, auth) — KPI: alert configurati su ogni metrica critica.
  • Revoca token e WebSocket testata end-to-end — KPI: tempo di revoca effettiva sotto i 2 secondi.

FAQ

SameSite=Strict o Lax per un cookie di sessione?

Lax è il compromesso corretto per la maggior parte delle applicazioni; Strict solo se non hai flussi con redirect da domini esterni verso l'app.

Perché non usare semplicemente sessionStorage invece di localStorage per il JWT?

Entrambi sono ugualmente leggibili da JavaScript e quindi vulnerabili a XSS: la differenza è solo la durata, non la sicurezza.

s-maxage funziona senza una CDN configurata?

No, è ignorato dai browser: ha effetto solo su cache condivise conformi come CDN o reverse proxy che lo supportano esplicitamente.

Come si revoca un JWT prima della scadenza?

Con uno stato lato server (blacklist o versione del token per utente), perché un JWT stateless puro non può essere invalidato prima della scadenza naturale.

Qual è un limite ragionevole di messaggi al secondo per un WebSocket?

Dipende dal caso d'uso, ma 10-30 messaggi/secondo per connessione è un punto di partenza ragionevole per la maggior parte delle applicazioni real-time.

OCSP stapling è obbligatorio?

Non obbligatorio ma fortemente consigliato: riduce la latenza dell'handshake TLS evitando che il client contatti separatamente l'authority di certificazione.

Come si evita che una CDN serva una risposta sensibile al cliente sbagliato?

Con Cache-Control: private o no-store sulle risposte personali, mai public quando i dati variano per utente.

Serve monitorare anche i fallimenti di refresh token?

Sì, un aumento improvviso dei fallimenti di refresh è spesso il primo segnale di un attacco in corso o di un bug di scadenza token.

Errori Comuni da Evitare

  • WebSocket in chiaro (ws://) dietro un frontend HTTPS: il browser lo permette solo se il WebSocket è sulla stessa origine non sicura, ma espone comunque dati in chiaro sulla rete.
  • Cookie senza Secure in sviluppo lasciato anche in produzione: spesso copiato da una configurazione di test mai aggiornata.
  • JWT in localStorage "temporaneamente" durante lo sviluppo: diventa quasi sempre permanente perché "funziona" e nessuno lo rivede più.
  • Cache-Control assente per default su nuove rotte API: senza un header esplicito, il comportamento di cache dipende dal client e non è garantito.
  • s-maxage e max-age identici: elimina il vantaggio di poter invalidare la cache browser più rapidamente di quella edge.
  • Nessuna dimensione massima sui messaggi WebSocket: un singolo client può inviare payload enormi e saturare la memoria del server.
  • Revoca che blocca solo le richieste HTTP future: lascia attive le connessioni WebSocket già aperte con lo stesso token compromesso.
  • Nessun monitoring su cache hit/miss: un ratio di hit in calo spesso segnala una regressione negli header di cache passata inosservata.

Risposte Rapide

Perché HTTPS è obbligatorio anche per WebSocket? Un WebSocket in chiaro (ws://) espone token di sessione e payload applicativi a chiunque sulla stessa rete; wss:// cifra l'intera connessione esattamente come HTTPS fa per le richieste HTTP tradizionali.

Cos'è il flag SameSite di un cookie? Controlla se un cookie viene inviato in richieste cross-site: Strict blocca l'invio cross-site, Lax lo permette solo per navigazione top-level, None lo permette sempre ma richiede Secure.

Perché non salvare un JWT in localStorage? Perché è leggibile da qualsiasi script JavaScript eseguito sulla pagina: un singolo XSS, anche in una libreria terza, permette di esfiltrare il token senza alcuna interazione dell'utente.

Cosa significa Cache-Control: immutable? Indica al browser che il contenuto di quell'URL non cambierà mai finché l'URL resta lo stesso, evitando persino una richiesta di validazione condizionale durante il periodo di cache.

Come si revoca una sessione con WebSocket attivi? Invalidando lo stato lato server del token (blacklist o versione) e chiudendo attivamente ogni connessione WebSocket associata a quell'utente, non solo rifiutando nuove richieste HTTP.

Qual è la differenza tra max-age e s-maxage? max-age controlla la cache del browser dell'utente, s-maxage controlla le cache condivise come CDN e reverse proxy, e quando presente ha precedenza su max-age per quelle cache.

Come Verificare

  • Verifica TLS e redirect HTTPS: curl -I https://tuo-dominio.com e controlla l'header Strict-Transport-Security.
  • Ispeziona certificato e protocollo TLS: openssl s_client -connect tuo-dominio.com:443 -tls1_2.
  • Controlla gli header di cache sugli asset: curl -I https://tuo-dominio.com/main.a1b2c3d4.js, verifica immutable e max-age.
  • Verifica che le risposte sensibili non siano cacheabili: curl -I https://tuo-dominio.com/api/me, controlla no-store.
  • Simula carico sul WebSocket per validare rate limiting: npx artillery quick --count 100 -n 20 wss://tuo-dominio.com/ws.
  • Testa la revoca end-to-end: revoca una sessione via API e verifica che il WebSocket associato si chiuda entro pochi secondi.

Conclusione

Nessuna di queste dieci pratiche è sufficiente da sola: TLS senza cookie configurati correttamente lascia comunque la sessione vulnerabile, caching aggressivo senza distinguere risposte pubbliche da private espone dati personali, e rate limiting senza monitoring non permette di sapere se sta funzionando davvero. Applicate insieme, con un piano di adozione a priorità (HTTPS e cookie prima, caching e rate limiting poi, monitoring continuo sempre), formano una base di sicurezza e performance coerente per qualsiasi applicazione web moderna con componenti real-time.

Vuoi una checklist stampabile o una valutazione della configurazione di sicurezza della tua applicazione? Richiedi un audit tecnico: in poche ore di analisi è possibile identificare i gap prioritari su cookie, caching, WebSocket e monitoring.

💬 Note dei lettori

0 note

Scrivi una nota

Condividi la tua opinione, un suggerimento o un complimento

Ultime note

Nessuna nota ancora. Sii il primo a commentare!