{"id":301,"date":"2026-06-21T16:33:58","date_gmt":"2026-06-21T16:33:58","guid":{"rendered":"https:\/\/shattered.io\/it\/2026\/06\/21\/websocket-sicuro-nodejs\/"},"modified":"2026-06-21T16:33:58","modified_gmt":"2026-06-21T16:33:58","slug":"websocket-sicuro-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/it\/2026\/06\/21\/websocket-sicuro-nodejs\/","title":{"rendered":"WebSocket Sicuro con Node.js: WSS, JWT e Rate Limiting in 12 Step [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Un&#8217;applicazione Node.js che usa WebSocket non autenticati \u00e8 come lasciare una porta sul retro aperta: chiunque pu\u00f2 entrare, restare quanto vuole e inviare qualsiasi messaggio. Secondo i dati del <strong>OWASP WebSocket Security Cheat Sheet<\/strong>, il 74% delle applicazioni real-time analizzate nel 2025 presentava almeno una vulnerabilit\u00e0 critica nella gestione delle connessioni WebSocket. Questo tutorial mostra come configurare WebSocket sicuri (WSS) in Node.js in 12 step, coprendo TLS, autenticazione JWT, validazione dell&#8217;Origin, rate limiting e protezione DoS.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"perche-i-websocket-non-sicuri-sono-pericolosi\">Perch\u00e9 i WebSocket non sicuri sono pericolosi<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I WebSocket stabiliscono un canale bidirezionale persistente tra client e server. A differenza delle normali richieste HTTP, una connessione WebSocket rimane aperta per minuti o ore, e ogni messaggio viene consegnato senza i meccanismi di sicurezza che i browser applicano alle richieste HTTP tradizionali (come le politiche CORS complete o la protezione CSRF automatica).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I principali vettori di attacco sui WebSocket includono:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Cross-Site WebSocket Hijacking (CSWSH):<\/strong> un sito malevolo apre una connessione WebSocket verso la tua applicazione usando i cookie di sessione dell&#8217;utente. L&#8217;RFC 6455 non impone all&#8217;implementazione server di verificare l&#8217;header <code>Origin<\/code>, quindi il default \u00e8 vulnerabile.<\/li>\n<li><strong>WebSocket DoS:<\/strong> un attaccante apre migliaia di connessioni o invia messaggi a raffica, esaurendo file descriptor e memoria del processo Node.js.<\/li>\n<li><strong>Iniezione di payload:<\/strong> senza validazione dei messaggi, un client autenticato ma compromesso pu\u00f2 inviare dati strutturati arbitrari (JSON injection, oversized payloads, payload contenenti codice malevolo).<\/li>\n<li><strong>Man-in-the-Middle su WS in chiaro:<\/strong> usare <code>ws:\/\/<\/code> invece di <code>wss:\/\/<\/code> espone tutti i messaggi alla cattura in rete.<\/li>\n<li><strong>Token riutilizzati su connessioni di lunga durata:<\/strong> un JWT valido al momento della connessione pu\u00f2 scadere ore dopo, lasciando connessioni zombie attive.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Nel 2025, PortSwigger ha documentato decine di vulnerabilit\u00e0 reali in applicazioni che usano WebSocket, tra cui bypass di autenticazione in piattaforme SaaS e data exfiltration via WebSocket non autenticati. La superficie di attacco \u00e8 reale e sottovalutata.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"prerequisiti-e-versioni\">Prerequisiti e versioni<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Prima di iniziare, assicurati di avere installato e configurato:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Componente<\/th><th>Versione richiesta<\/th><th>Note<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>v20.x LTS o v22.x LTS<\/td><td>Security patch gennaio 2026 incluse<\/td><\/tr><tr><td>npm<\/td><td>v10.x<\/td><td>Usa <code>npm ci<\/code> per lockfile<\/td><\/tr><tr><td>ws<\/td><td>8.21.0<\/td><td>Libreria WebSocket core per Node.js<\/td><\/tr><tr><td>socket.io<\/td><td>4.8.3<\/td><td>Opzionale, per ambienti con proxy\/LB<\/td><\/tr><tr><td>jsonwebtoken<\/td><td>9.0.3<\/td><td>Autenticazione JWT sull&#8217;handshake<\/td><\/tr><tr><td>helmet<\/td><td>8.2.0<\/td><td>Security headers per Express<\/td><\/tr><tr><td>express-rate-limit<\/td><td>8.5.2<\/td><td>Rate limiting sulle richieste HTTP di upgrade<\/td><\/tr><tr><td>OpenSSL<\/td><td>3.x<\/td><td>Generazione certificati TLS<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Conoscenze necessarie: basi di Node.js e Express, familiarit\u00e0 con JWT e TLS, comprensione del protocollo HTTP e dell&#8217;handshake WebSocket (RFC 6455). Non \u00e8 richiesta esperienza pregressa con WebSocket sicuri.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-1-inizializza-il-progetto-e-installa-le-dipendenze\">Step 1: Inizializza il progetto e installa le dipendenze<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Crea una directory di progetto pulita e installa tutte le dipendenze necessarie. L&#8217;uso di <code>npm ci<\/code> in produzione garantisce che solo le versioni esatte nel lockfile vengano installate, eliminando la variabilit\u00e0 delle dipendenze.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir wss-secure-demo && cd wss-secure-demo\nnpm init -y\n\n# Dipendenze principali\nnpm install ws@8.21.0 express helmet express-rate-limit jsonwebtoken dotenv\n\n# Dipendenze opzionali per performance WebSocket\nnpm install bufferutil@4.0.9 utf-8-validate@6.0.6\n\necho \"node_modules\/\" >> .gitignore\necho \".env\" >> .gitignore<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Crea il file <code>.env<\/code> per le variabili d&#8217;ambiente sensibili. Non committare mai questo file nel repository. Il segreto JWT deve essere lungo almeno 32 caratteri e generato in modo casuale:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .env\nJWT_SECRET=genera_un_segreto_casuale_di_almeno_32_caratteri_qui\nPORT=3000\nWSS_PORT=3001\nNODE_ENV=production\nALLOWED_ORIGINS=https:\/\/tuodominio.it,https:\/\/www.tuodominio.it<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Per generare un segreto JWT robusto, usa il modulo <code>crypto<\/code> di Node.js: <code>node -e \"console.log(require('crypto').randomBytes(64).toString('hex'))\"<\/code>. Non usare mai stringhe prevedibili come &#8220;secret&#8221; o &#8220;password&#8221; come JWT_SECRET in produzione.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-2-genera-un-certificato-tls-per-wss\">Step 2: Genera un certificato TLS per WSS<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">WebSocket sicuri (WSS) richiedono TLS, esattamente come HTTPS richiede TLS rispetto a HTTP. In sviluppo, genera un certificato self-signed con OpenSSL. In produzione, usa Let&#8217;s Encrypt tramite Certbot o un certificato emesso da una CA commerciale.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Genera chiave privata RSA 4096-bit e certificato self-signed per sviluppo\nopenssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \\\n  -days 365 -nodes \\\n  -subj \"\/C=IT\/ST=Italy\/L=Milano\/O=Demo\/CN=localhost\"\n\n# Imposta permessi restrittivi sulla chiave privata\nchmod 600 key.pem\n\n# Verifica il certificato\nopenssl x509 -in cert.pem -text -noout | grep -E \"Subject:|Not (Before|After)\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In produzione con Let&#8217;s Encrypt, i file si trovano in <code>\/etc\/letsencrypt\/live\/tuodominio.it\/<\/code>. Configura il rinnovo automatico con <code>certbot renew<\/code> via cron per evitare che il certificato scada e rompa le connessioni WSS esistenti. Un certificato scaduto causa errori <code>ERR_CERT_DATE_INVALID<\/code> nei client e disconnette tutte le sessioni WebSocket attive.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-3-configura-il-server-https-e-websocket\">Step 3: Configura il server HTTPS e WebSocket<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Il server WSS viene costruito sopra un server HTTPS Node.js standard. La libreria <code>ws<\/code> accetta qualsiasi istanza di server HTTP\/HTTPS e gestisce l&#8217;handshake WebSocket (l&#8217;upgrade da HTTP a WebSocket) automaticamente.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ server.js\nrequire('dotenv').config();\nconst https = require('https');\nconst fs = require('fs');\nconst express = require('express');\nconst helmet = require('helmet');\nconst { WebSocketServer } = require('ws');\n\nconst app = express();\n\n\/\/ Helmet imposta security headers su tutte le risposte HTTP\napp.use(helmet());\napp.use(express.json());\n\n\/\/ Carica certificati TLS\nconst tlsOptions = {\n  key: fs.readFileSync('.\/key.pem'),\n  cert: fs.readFileSync('.\/cert.pem'),\n  \/\/ Forza TLS 1.2 minimo (TLS 1.3 preferito)\n  minVersion: 'TLSv1.2',\n  \/\/ Disabilita cifrature deboli\n  ciphers: [\n    'TLS_AES_256_GCM_SHA384',\n    'TLS_CHACHA20_POLY1305_SHA256',\n    'ECDHE-RSA-AES256-GCM-SHA384'\n  ].join(':')\n};\n\nconst server = https.createServer(tlsOptions, app);\n\n\/\/ Server WebSocket agganciato al server HTTPS\nconst wss = new WebSocketServer({\n  server,\n  maxPayload: 65536, \/\/ Max 64KB per messaggio - protezione DoS\n  clientTracking: true\n});\n\nserver.listen(process.env.PORT || 3000, () => {\n  console.log(`WSS server attivo su wss:\/\/localhost:${process.env.PORT || 3000}`);\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Il parametro <code>maxPayload<\/code> \u00e8 fondamentale: senza di esso, un client malevolo pu\u00f2 inviare un singolo messaggio da gigabyte, saturando la memoria del processo Node.js. Il valore di 65536 byte (64KB) \u00e8 sufficiente per la maggior parte delle applicazioni real-time. Adattalo in base ai requisiti specifici, ma non impostarlo mai a <code>Infinity<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-4-implementa-lautenticazione-jwt-sullhandshake\">Step 4: Implementa l&#8217;autenticazione JWT sull&#8217;handshake<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;handshake WebSocket inizia con una richiesta HTTP GET che include l&#8217;header <code>Upgrade: websocket<\/code>. Questo \u00e8 il momento ideale per verificare l&#8217;identit\u00e0 del client: se la validazione fallisce, la connessione viene rifiutata prima che venga stabilita, risparmiando risorse server.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ci sono due pattern principali per passare il token JWT durante l&#8217;handshake:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Query string:<\/strong> <code>wss:\/\/server.it\/ws?token=eyJ...<\/code> &#8211; semplice ma il token appare nei log del server<\/li>\n<li><strong>Subprotocol header:<\/strong> il token viene codificato nel header <code>Sec-WebSocket-Protocol<\/code> &#8211; non appare nei log ma richiede logica custom<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ auth.js\nconst jwt = require('jsonwebtoken');\n\n\/**\n * Verifica il token JWT dalla query string dell'handshake WebSocket.\n * Restituisce il payload decodificato o lancia un errore.\n *\/\nfunction verifyWebSocketToken(request) {\n  const url = new URL(request.url, `https:\/\/${request.headers.host}`);\n  const token = url.searchParams.get('token');\n\n  if (!token) {\n    throw new Error('Token assente');\n  }\n\n  \/\/ jwt.verify lancia JsonWebTokenError se il token non \u00e8 valido\n  \/\/ e TokenExpiredError se \u00e8 scaduto\n  const payload = jwt.verify(token, process.env.JWT_SECRET, {\n    algorithms: ['HS256'],       \/\/ Accetta solo HS256, mai 'none'\n    issuer: 'tuodominio.it',     \/\/ Valida l'issuer\n    audience: 'websocket-api'   \/\/ Valida l'audience\n  });\n\n  return payload;\n}\n\nmodule.exports = { verifyWebSocketToken };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Collega la funzione di autenticazione all&#8217;evento <code>connection<\/code> del WebSocketServer. La verifica deve avvenire prima che qualsiasi messaggio venga processato:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Nel server.js, aggiungi il gestore delle connessioni\nconst { verifyWebSocketToken } = require('.\/auth');\n\nwss.on('connection', (ws, request) => {\n  \/\/ Tentativo di autenticazione - rifiuta se il token non \u00e8 valido\n  let user;\n  try {\n    user = verifyWebSocketToken(request);\n  } catch (err) {\n    ws.close(1008, 'Non autorizzato'); \/\/ 1008 = Policy Violation\n    return;\n  }\n\n  \/\/ Memorizza l'identit\u00e0 sul socket per uso nei message handler\n  ws.userId = user.sub;\n  ws.userRole = user.role;\n\n  console.log(`Connessione WebSocket autenticata: user=${user.sub}`);\n\n  ws.on('message', (message) => {\n    handleMessage(ws, message, user);\n  });\n\n  ws.on('close', () => {\n    console.log(`Disconnessione: user=${user.sub}`);\n  });\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-5-valida-lheader-origin-per-prevenire-cswsh\">Step 5: Valida l&#8217;header Origin per prevenire CSWSH<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Il Cross-Site WebSocket Hijacking (CSWSH) \u00e8 l&#8217;equivalente WebSocket dell&#8217;attacco CSRF. Un sito malevolo su <code>attacker.com<\/code> pu\u00f2 aprire una connessione WebSocket verso <code>tuoserver.it<\/code> usando i cookie dell&#8217;utente. Il browser include automaticamente i cookie nelle richieste WebSocket, proprio come nelle richieste HTTP normali.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La difesa \u00e8 validare l&#8217;header <code>Origin<\/code> durante l&#8217;handshake usando l&#8217;opzione <code>verifyClient<\/code> della libreria <code>ws<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ origin-validator.js\nconst ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || '')\n  .split(',')\n  .map(o => o.trim())\n  .filter(Boolean);\n\n\/**\n * Verifica che l'Origin della richiesta sia nella whitelist.\n * Chiamata durante l'handshake, prima che la connessione venga accettata.\n *\/\nfunction verifyOrigin(info, callback) {\n  const origin = info.origin;\n\n  \/\/ In sviluppo, accetta localhost\n  if (process.env.NODE_ENV !== 'production' && \n      (origin === 'https:\/\/localhost:3000' || origin === 'http:\/\/localhost:3000')) {\n    return callback(true);\n  }\n\n  if (ALLOWED_ORIGINS.includes(origin)) {\n    return callback(true);\n  }\n\n  console.warn(`Connessione WebSocket rifiutata - Origin non autorizzato: ${origin}`);\n  callback(false, 403, 'Origin non autorizzato');\n}\n\nmodule.exports = { verifyOrigin };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Integra <code>verifyOrigin<\/code> nella configurazione del WebSocketServer:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const { verifyOrigin } = require('.\/origin-validator');\n\nconst wss = new WebSocketServer({\n  server,\n  maxPayload: 65536,\n  clientTracking: true,\n  verifyClient: verifyOrigin  \/\/ Chiamata per ogni tentativo di handshake\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-6-implementa-il-rate-limiting-sulle-connessioni\">Step 6: Implementa il rate limiting sulle connessioni<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Il rate limiting sui WebSocket ha due livelli distinti: limitare il numero di connessioni per IP (per prevenire connection flooding) e limitare il numero di messaggi per connessione (per prevenire message flooding). Entrambi sono necessari.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Per il rate limiting sulle richieste di upgrade HTTP (che avvengono prima dell&#8217;handshake WebSocket), usa <code>express-rate-limit<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ rate-limiter.js\nconst rateLimit = require('express-rate-limit');\n\n\/\/ Limita le richieste di upgrade WebSocket: max 10 connessioni\/IP\/15 minuti\nconst wsUpgradeLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 10,\n  message: { error: 'Troppe connessioni, riprova tra 15 minuti' },\n  standardHeaders: true,\n  legacyHeaders: false\n});\n\n\/\/ Map per il rate limiting per-messaggio: userId -> { count, resetTime }\nconst messageRateLimits = new Map();\n\n\/**\n * Verifica se un utente ha superato il limite di messaggi.\n * Implementa l'algoritmo token bucket semplificato.\n *\/\nfunction checkMessageRateLimit(userId, maxMessages = 60, windowMs = 60000) {\n  const now = Date.now();\n  const userLimit = messageRateLimits.get(userId);\n\n  if (!userLimit || now > userLimit.resetTime) {\n    messageRateLimits.set(userId, { count: 1, resetTime: now + windowMs });\n    return true; \/\/ OK\n  }\n\n  if (userLimit.count >= maxMessages) {\n    return false; \/\/ Limite superato\n  }\n\n  userLimit.count++;\n  return true;\n}\n\n\/\/ Pulizia periodica della Map (ogni ora) per evitare memory leak\nsetInterval(() => {\n  const now = Date.now();\n  for (const [userId, limit] of messageRateLimits.entries()) {\n    if (now > limit.resetTime) messageRateLimits.delete(userId);\n  }\n}, 60 * 60 * 1000);\n\nmodule.exports = { wsUpgradeLimiter, checkMessageRateLimit };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Applica il rate limiter HTTP agli endpoint che gestiscono l&#8217;upgrade WebSocket:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const { wsUpgradeLimiter, checkMessageRateLimit } = require('.\/rate-limiter');\n\n\/\/ L'endpoint \/ws viene chiamato per l'upgrade HTTP->WebSocket\napp.use('\/ws', wsUpgradeLimiter);<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-7-valida-e-sanitizza-tutti-i-messaggi-in-ingresso\">Step 7: Valida e sanitizza tutti i messaggi in ingresso<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Non fidarti mai dei dati inviati dai client WebSocket, nemmeno da client autenticati. Un account compromesso, un bug nel client o un test di sicurezza possono inviare payload malformati, oversized o strutturati per sfruttare vulnerabilit\u00e0 nel codice di parsing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Definisci uno schema per ogni tipo di messaggio e valida rigorosamente:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ message-handler.js\nconst { checkMessageRateLimit } = require('.\/rate-limiter');\n\n\/\/ Tipi di messaggio ammessi\nconst MESSAGE_TYPES = new Set(['chat', 'ping', 'subscribe', 'unsubscribe']);\n\n\/\/ Lunghezza massima per i campi testo\nconst MAX_TEXT_LENGTH = 1000;\n\nfunction handleMessage(ws, rawMessage, user) {\n  \/\/ 1. Controllo rate limiting per messaggio\n  if (!checkMessageRateLimit(user.sub)) {\n    ws.send(JSON.stringify({ type: 'error', message: 'Troppi messaggi, rallenta' }));\n    return;\n  }\n\n  \/\/ 2. Parsing JSON sicuro\n  let message;\n  try {\n    \/\/ Rifiuta messaggi non-stringa (es. Buffer binari non attesi)\n    if (typeof rawMessage !== 'string' && !Buffer.isBuffer(rawMessage)) {\n      throw new Error('Formato messaggio non supportato');\n    }\n    const text = Buffer.isBuffer(rawMessage) ? rawMessage.toString('utf8') : rawMessage;\n    message = JSON.parse(text);\n  } catch {\n    ws.send(JSON.stringify({ type: 'error', message: 'JSON non valido' }));\n    return;\n  }\n\n  \/\/ 3. Validazione struttura base\n  if (!message || typeof message !== 'object' || Array.isArray(message)) {\n    ws.send(JSON.stringify({ type: 'error', message: 'Struttura messaggio non valida' }));\n    return;\n  }\n\n  \/\/ 4. Validazione tipo messaggio\n  if (!MESSAGE_TYPES.has(message.type)) {\n    ws.send(JSON.stringify({ type: 'error', message: 'Tipo messaggio non riconosciuto' }));\n    return;\n  }\n\n  \/\/ 5. Validazione campi specifici per tipo\n  if (message.type === 'chat') {\n    if (typeof message.content !== 'string' || message.content.length > MAX_TEXT_LENGTH) {\n      ws.send(JSON.stringify({ type: 'error', message: 'Contenuto non valido o troppo lungo' }));\n      return;\n    }\n    \/\/ Sanitizzazione base: rimuovi tag HTML per prevenire XSS se il testo\n    \/\/ verr\u00e0 mai mostrato nel DOM senza encoding\n    message.content = message.content.replace(\/<[^>]*>\/g, '');\n  }\n\n  \/\/ 6. Dispatch al handler corretto\n  switch (message.type) {\n    case 'chat':\n      handleChatMessage(ws, message, user);\n      break;\n    case 'ping':\n      ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));\n      break;\n    \/\/ ... altri handler\n  }\n}\n\nmodule.exports = { handleMessage };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-8-gestisci-ping-pong-per-rilevare-connessioni-morte\">Step 8: Gestisci ping\/pong per rilevare connessioni morte<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Una connessione WebSocket pu\u00f2 sembrare attiva dal punto di vista del server anche quando il client si \u00e8 disconnesso (ad esempio per caduta di rete, chiusura del tab del browser). Connessioni zombie consumano memoria e file descriptor. Il meccanismo ping\/pong del protocollo WebSocket (RFC 6455) permette al server di verificare periodicamente se il client \u00e8 ancora vivo.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ heartbeat.js - Ping\/Pong per rilevare connessioni zombie\n\nfunction setupHeartbeat(wss, intervalMs = 30000) {\n  \/\/ Ogni 30 secondi, invia un ping a tutti i client\n  const interval = setInterval(() => {\n    wss.clients.forEach((ws) => {\n      if (ws.isAlive === false) {\n        \/\/ Client non ha risposto al ping precedente: termina la connessione\n        console.log(`Terminazione connessione zombie: user=${ws.userId}`);\n        return ws.terminate(); \/\/ terminate() \u00e8 pi\u00f9 aggressivo di close()\n      }\n\n      \/\/ Segna come \"non risposto\" prima di inviare il ping\n      ws.isAlive = false;\n      ws.ping(); \/\/ Invia frame ping WebSocket (non un messaggio applicativo)\n    });\n  }, intervalMs);\n\n  \/\/ Ferma il heartbeat quando il server si chiude\n  wss.on('close', () => clearInterval(interval));\n\n  return interval;\n}\n\n\/\/ Nel gestore di connessione, aggiungi:\n\/\/ ws.isAlive = true;\n\/\/ ws.on('pong', () => { ws.isAlive = true; });\n\nmodule.exports = { setupHeartbeat };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Integra il heartbeat nel server principale:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const { setupHeartbeat } = require('.\/heartbeat');\n\nwss.on('connection', (ws, request) => {\n  \/\/ ... autenticazione (step 4)\n\n  ws.isAlive = true;\n  ws.on('pong', () => { ws.isAlive = true; }); \/\/ Risposta al ping del server\n\n  \/\/ ... message handler\n});\n\n\/\/ Avvia heartbeat dopo aver configurato il server\nsetupHeartbeat(wss, 30000); \/\/ Check ogni 30 secondi<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-9-limita-il-numero-massimo-di-connessioni-concorrenti\">Step 9: Limita il numero massimo di connessioni concorrenti<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ogni connessione WebSocket occupa un file descriptor del sistema operativo. Il default per molti sistemi Linux \u00e8 <code>ulimit -n 1024<\/code>, il che significa che 1024 connessioni simultanee possono esaurire i file descriptor disponibili per il processo Node.js, causando errori <code>EMFILE: too many open files<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Aggiungi un controllo esplicito sul numero massimo di connessioni nel gestore <code>connection<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const MAX_CONNECTIONS = parseInt(process.env.MAX_WS_CONNECTIONS || '500');\nconst MAX_CONNECTIONS_PER_USER = 3;\n\n\/\/ Map: userId -> Set di connessioni attive\nconst userConnections = new Map();\n\nwss.on('connection', (ws, request) => {\n  \/\/ Check globale\n  if (wss.clients.size > MAX_CONNECTIONS) {\n    ws.close(1013, 'Server al massimo della capacit\u00e0'); \/\/ 1013 = Try Again Later\n    return;\n  }\n\n  \/\/ ... autenticazione JWT (step 4)\n  let user;\n  try {\n    user = verifyWebSocketToken(request);\n  } catch {\n    ws.close(1008, 'Non autorizzato');\n    return;\n  }\n\n  \/\/ Check per-utente (prevenzione connessioni multiple abusive)\n  const userConns = userConnections.get(user.sub) || new Set();\n  if (userConns.size >= MAX_CONNECTIONS_PER_USER) {\n    ws.close(1008, 'Troppe connessioni per questo utente');\n    return;\n  }\n\n  userConns.add(ws);\n  userConnections.set(user.sub, userConns);\n  ws.userId = user.sub;\n\n  ws.on('close', () => {\n    const conns = userConnections.get(user.sub);\n    if (conns) {\n      conns.delete(ws);\n      if (conns.size === 0) userConnections.delete(user.sub);\n    }\n  });\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-10-implementa-il-logging-sicuro-degli-eventi\">Step 10: Implementa il logging sicuro degli eventi<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Il logging \u00e8 essenziale per l&#8217;audit e la risposta agli incidenti, ma un logging mal fatto pu\u00f2 diventare un vettore di vulnerabilit\u00e0: le credenziali degli utenti, i token JWT o i dati sensibili nei messaggi non devono mai finire nei log.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Evento<\/th><th>Cosa loggare<\/th><th>Cosa NON loggare<\/th><\/tr><\/thead><tbody><tr><td>Connessione<\/td><td>userId, IP (anonimizzato), timestamp, user-agent<\/td><td>Token JWT completo, password, cookie<\/td><\/tr><tr><td>Autenticazione fallita<\/td><td>IP, tipo di errore, timestamp<\/td><td>Token scaduto (il token stesso), credenziali<\/td><\/tr><tr><td>Messaggio ricevuto<\/td><td>userId, tipo messaggio, lunghezza payload<\/td><td>Contenuto del messaggio (dati utente)<\/td><\/tr><tr><td>Rate limit superato<\/td><td>userId, IP, contatore, finestra temporale<\/td><td>Contenuto dei messaggi rifiutati<\/td><\/tr><tr><td>Disconnessione<\/td><td>userId, durata sessione, codice chiusura<\/td><td>Dati di sessione, messaggi in cache<\/td><\/tr><tr><td>Errore applicativo<\/td><td>Tipo errore, stack trace (solo in dev), timestamp<\/td><td>Dati utente nell&#8217;errore, segreti<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ logger.js - Logging strutturato e sicuro\nfunction createSecureLogger() {\n  function anonymizeIp(ip) {\n    if (!ip) return 'unknown';\n    \/\/ Anonimizza l'ultimo ottetto dell'IPv4 (o l'ultimo gruppo IPv6)\n    return ip.replace(\/(\\d+)$\/, '0').replace(\/:[0-9a-f]+$\/, ':0');\n  }\n\n  return {\n    connectionAccepted: (userId, request) => {\n      const ip = request.socket.remoteAddress;\n      console.log(JSON.stringify({\n        event: 'ws_connection_accepted',\n        userId,\n        ip: anonymizeIp(ip),\n        userAgent: request.headers['user-agent']?.substring(0, 100),\n        timestamp: new Date().toISOString()\n      }));\n    },\n\n    authFailed: (reason, request) => {\n      const ip = request.socket.remoteAddress;\n      console.warn(JSON.stringify({\n        event: 'ws_auth_failed',\n        reason, \/\/ 'token_missing', 'token_expired', 'token_invalid', 'origin_blocked'\n        ip: anonymizeIp(ip),\n        timestamp: new Date().toISOString()\n      }));\n    },\n\n    messageReceived: (userId, messageType, payloadLength) => {\n      console.log(JSON.stringify({\n        event: 'ws_message_received',\n        userId,\n        messageType,\n        payloadLength,\n        timestamp: new Date().toISOString()\n      }));\n    }\n  };\n}\n\nmodule.exports = { createSecureLogger };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-11-genera-token-jwt-per-i-client\">Step 11: Genera token JWT per i client<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I client WebSocket devono ottenere un token JWT prima di tentare la connessione. La distribuzione del token avviene tipicamente tramite un endpoint HTTP REST protetto, dopo che l&#8217;utente ha completato il login. Il token deve avere una scadenza breve per ridurre la finestra di attacco in caso di furto.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ routes\/auth.js - Endpoint HTTP per il login e rilascio del token\nconst jwt = require('jsonwebtoken');\nconst { wsUpgradeLimiter } = require('..\/rate-limiter');\n\n\/\/ Simulazione database utenti (in produzione usa bcrypt + DB)\nconst USERS = {\n  'user123': { password: 'hashed_in_production', role: 'user' },\n};\n\napp.post('\/api\/login', async (req, res) => {\n  const { username, password } = req.body;\n\n  if (!USERS[username] || password !== 'demo') {\n    return res.status(401).json({ error: 'Credenziali non valide' });\n  }\n\n  \/\/ Token con scadenza 1 ora - breve per minimizzare il rischio\n  const token = jwt.sign(\n    {\n      sub: username,\n      role: USERS[username].role,\n      iss: 'tuodominio.it',\n      aud: 'websocket-api'\n    },\n    process.env.JWT_SECRET,\n    {\n      algorithm: 'HS256',\n      expiresIn: '1h'\n    }\n  );\n\n  \/\/ Non restituire il token in un cookie: i cookie vengono inviati\n  \/\/ automaticamente nelle richieste WebSocket e sono vulnerabili a CSWSH.\n  \/\/ Usa il corpo della risposta e memorizza il token in memoria JavaScript.\n  res.json({ token, expiresIn: 3600 });\n});\n\n\/\/ Endpoint per il rinnovo del token (refresh) prima della scadenza\napp.post('\/api\/token\/refresh', (req, res) => {\n  const authHeader = req.headers.authorization;\n  if (!authHeader?.startsWith('Bearer ')) {\n    return res.status(401).json({ error: 'Token assente' });\n  }\n\n  try {\n    const oldToken = authHeader.slice(7);\n    \/\/ Verifica senza controllo scadenza per permettere il rinnovo\n    const payload = jwt.verify(oldToken, process.env.JWT_SECRET, {\n      ignoreExpiration: true,\n      algorithms: ['HS256'],\n      issuer: 'tuodominio.it',\n      audience: 'websocket-api'\n    });\n\n    \/\/ Limita il rinnovo a token scaduti da meno di 24 ore\n    const expiredAt = payload.exp * 1000;\n    if (Date.now() - expiredAt > 24 * 60 * 60 * 1000) {\n      return res.status(401).json({ error: 'Token troppo vecchio per il rinnovo' });\n    }\n\n    const newToken = jwt.sign(\n      { sub: payload.sub, role: payload.role, iss: payload.iss, aud: payload.aud },\n      process.env.JWT_SECRET,\n      { algorithm: 'HS256', expiresIn: '1h' }\n    );\n\n    res.json({ token: newToken, expiresIn: 3600 });\n  } catch {\n    res.status(401).json({ error: 'Token non valido' });\n  }\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-12-testa-la-sicurezza-con-wscat-e-script-di-verifica\">Step 12: Testa la sicurezza con wscat e script di verifica<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Prima di mettere in produzione il server WebSocket, verifica ogni controllo di sicurezza implementato. Usa <code>wscat<\/code> (installabile con <code>npm install -g wscat<\/code>) per testare manualmente le connessioni WebSocket da linea di comando.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Test 1: Connessione senza token (deve fallire con codice 1008)\nwscat -c \"wss:\/\/localhost:3000\/ws\" --no-check\n# Risultato atteso: error: Websocket: Error: 401 Non autorizzato\n\n# Test 2: Connessione con token valido\nTOKEN=$(curl -sk -X POST https:\/\/localhost:3000\/api\/login \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"username\":\"user123\",\"password\":\"demo\"}' | python3 -c \"import json,sys; print(json.load(sys.stdin)['token'])\")\nwscat -c \"wss:\/\/localhost:3000\/ws?token=$TOKEN\" --no-check\n\n# Test 3: Verifica Origin validation (simula richiesta da origine non autorizzata)\nwscat -c \"wss:\/\/localhost:3000\/ws?token=$TOKEN\" \\\n  --header \"Origin: https:\/\/attacker-site.com\" --no-check\n# Risultato atteso: 403 Origin non autorizzato\n\n# Test 4: Invio payload oversized (deve essere rifiutato da maxPayload)\n# Genera stringa da 70KB e inviala\npython3 -c \"import json; print(json.dumps({'type':'chat','content':'A'*70000}))\" | wscat -c \"wss:\/\/localhost:3000\/ws?token=$TOKEN\" --no-check\n# Risultato atteso: WebSocket closed with reason: Max payload exceeded\n\n# Test 5: Rate limiting messaggi (invia 70 messaggi in 1 minuto)\nfor i in $(seq 1 70); do\n  echo '{\"type\":\"chat\",\"content\":\"test\"}' | wscat -c \"wss:\/\/localhost:3000\/ws?token=$TOKEN\" --no-check 2>\/dev\/null\ndone\necho \"Dopo 60 messaggi deve apparire: Troppi messaggi, rallenta\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Per un test di carico (verifica del limite di connessioni), usa <code>artillery<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\"># Installa artillery per test di carico\nnpm install -g artillery\n\n# Crea un file di configurazione artillery-ws.yml per simulare 600 connessioni\n# e verificare che il server risponda con 1013 dopo le prime 500<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"output-atteso-del-server-sicuro\">Output atteso del server sicuro<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Un server WSS configurato correttamente produce log strutturati come questi per ogni evento significativo. Questi log sono essenziali per il monitoraggio e la risposta agli incidenti:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Log tipici di un server WSS sicuro in produzione\n\n{\"event\":\"ws_connection_accepted\",\"userId\":\"user123\",\"ip\":\"192.168.1.0\",\"userAgent\":\"Mozilla\/5.0...\",\"timestamp\":\"2026-06-21T10:30:00.000Z\"}\n\n{\"event\":\"ws_auth_failed\",\"reason\":\"token_expired\",\"ip\":\"10.0.0.0\",\"timestamp\":\"2026-06-21T10:30:05.000Z\"}\n\n{\"event\":\"ws_auth_failed\",\"reason\":\"origin_blocked\",\"ip\":\"198.51.100.0\",\"timestamp\":\"2026-06-21T10:30:10.000Z\"}\n\n{\"event\":\"ws_message_received\",\"userId\":\"user123\",\"messageType\":\"chat\",\"payloadLength\":42,\"timestamp\":\"2026-06-21T10:30:15.000Z\"}\n\n{\"event\":\"ws_rate_limit_exceeded\",\"userId\":\"user456\",\"ip\":\"203.0.113.0\",\"count\":61,\"window\":\"60s\",\"timestamp\":\"2026-06-21T10:31:00.000Z\"}\n\n# Connessione zombie terminata dal heartbeat\nTerminazione connessione zombie: user=user789\n\n# Tentativo di connessione da Origin non autorizzato\nConnessione WebSocket rifiutata - Origin non autorizzato: https:\/\/attacker-site.com<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"errori-comuni-e-come-evitarli\">Errori comuni e come evitarli<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Questi sono i 7 errori pi\u00f9 frequenti nelle implementazioni WebSocket Node.js che causano vulnerabilit\u00e0 di sicurezza reali:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Errore<\/th><th>Rischio<\/th><th>Soluzione<\/th><\/tr><\/thead><tbody><tr><td>Usare <code>ws:\/\/<\/code> invece di <code>wss:\/\/<\/code><\/td><td>Intercettazione di tutti i messaggi in chiaro (MITM)<\/td><td>Sempre TLS: <code>wss:\/\/<\/code> in produzione, <code>https.createServer()<\/code><\/td><\/tr><tr><td>Non validare l&#8217;header Origin<\/td><td>CSWSH: siti malevoli aprono connessioni a nome dell&#8217;utente<\/td><td>Whitelist di origini con <code>verifyClient<\/code><\/td><\/tr><tr><td>Non impostare <code>maxPayload<\/code><\/td><td>DoS: singolo messaggio enorme satur\u00f2 la RAM<\/td><td>Imposta <code>maxPayload: 65536<\/code> (o meno)<\/td><\/tr><tr><td>Accettare algoritmo JWT <code>'none'<\/code><\/td><td>Bypass autenticazione: token non firmati accettati<\/td><td>Specifica sempre <code>algorithms: ['HS256']<\/code><\/td><\/tr><tr><td>Non implementare heartbeat<\/td><td>Memory leak: connessioni zombie accumulano in memoria<\/td><td>Ping ogni 30s, terminate se non risponde<\/td><\/tr><tr><td>Loggare il token JWT completo<\/td><td>Furto di token dai log, accesso non autorizzato<\/td><td>Logga solo userId e tipo di errore, mai il token<\/td><\/tr><tr><td>Non rinnovare il token durante la connessione<\/td><td>Sessioni con token scaduti rimangono attive per ore<\/td><td>Implementa endpoint di refresh e verifica periodica<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"pitfall-1-cookie-vs-header-per-il-token\">Pitfall 1: Cookie vs header per il token<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Molti tutorial suggeriscono di usare cookie <code>HttpOnly<\/code> per trasmettere il JWT ai WebSocket. Questo \u00e8 un errore: i cookie vengono inviati automaticamente dal browser in tutte le richieste verso il dominio, incluse le richieste WebSocket da pagine di terze parti, rendendo l&#8217;applicazione vulnerabile a CSWSH anche se la validazione dell&#8217;Origin \u00e8 assente o incorretta. Usa invece l&#8217;invio esplicito del token via query string o subprotocol header, combinato con la validazione dell&#8217;Origin.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"pitfall-2-validare-il-token-solo-allhandshake\">Pitfall 2: Validare il token solo all&#8217;handshake<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Un JWT viene verificato una sola volta all&#8217;apertura della connessione. Se la connessione dura 2 ore e il token ha scadenza di 1 ora, dopo 60 minuti la connessione \u00e8 tecnicamente aperta con un token scaduto. In scenari ad alto rischio (admin panel, operazioni finanziarie), implementa un check periodico della validit\u00e0 del token e chiudi le connessioni con token scaduti: <code>if (Date.now() > user.exp * 1000) ws.close(1008, 'Token scaduto');<\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"troubleshooting-8-problemi-comuni\">Troubleshooting: 8 problemi comuni<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>#<\/th><th>Errore<\/th><th>Causa<\/th><th>Soluzione<\/th><\/tr><\/thead><tbody><tr><td>1<\/td><td><code>Error: self signed certificate<\/code><\/td><td>Client rifiuta il certificato self-signed<\/td><td>Usa <code>wscat --no-check<\/code> in sviluppo. In produzione usa Let&#8217;s Encrypt.<\/td><\/tr><tr><td>2<\/td><td><code>WebSocket is closed before the connection is established<\/code><\/td><td>Server chiude immediatamente (auth fallita o Origin bloccato)<\/td><td>Controlla i log server per vedere il motivo del rifiuto (<code>1008<\/code> o <code>403<\/code>)<\/td><\/tr><tr><td>3<\/td><td><code>EMFILE: too many open files<\/code><\/td><td>File descriptor esauriti (ulimit troppo basso)<\/td><td>Aumenta <code>ulimit -n 65535<\/code> e imposta <code>MAX_CONNECTIONS<\/code> nel codice<\/td><\/tr><tr><td>4<\/td><td><code>JsonWebTokenError: invalid signature<\/code><\/td><td>JWT_SECRET diverso tra emissione e verifica (es. variabili env diverse)<\/td><td>Verifica che JWT_SECRET sia identico in tutti i processi. Usa <code>dotenv<\/code>.<\/td><\/tr><tr><td>5<\/td><td><code>TokenExpiredError<\/code> su connessione appena aperta<\/td><td>Orologio del server sfasato (clock skew)<\/td><td>Aggiungi <code>clockTolerance: 30<\/code> in <code>jwt.verify()<\/code> o sincronizza NTP<\/td><\/tr><tr><td>6<\/td><td>Connessioni non si chiudono dopo timeout<\/td><td>Heartbeat non configurato o evento <code>pong<\/code> non registrato<\/td><td>Verifica che <code>ws.on('pong', ...)<\/code> sia aggiunto nel gestore <code>connection<\/code><\/td><\/tr><tr><td>7<\/td><td>Messaggi grandi troncati a 64KB<\/td><td><code>maxPayload<\/code> troppo basso per il caso d&#8217;uso<\/td><td>Aumenta <code>maxPayload<\/code> o implementa messaggi chunked lato client<\/td><\/tr><tr><td>8<\/td><td>Rate limiting non funziona con proxy\/load balancer<\/td><td>IP del client \u00e8 l&#8217;IP del proxy (tutti appaiono come stesso IP)<\/td><td>Configura <code>trust proxy: true<\/code> in Express e usa l&#8217;header <code>X-Forwarded-For<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"tecniche-avanzate-per-ambienti-di-produzione\">Tecniche avanzate per ambienti di produzione<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"websocket-con-socket-io-in-ambienti-con-load-balancer\">WebSocket con Socket.IO in ambienti con load balancer<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In ambienti con multiple istanze Node.js dietro un load balancer, la libreria <code>ws<\/code> non scala orizzontalmente perch\u00e9 lo stato delle connessioni \u00e8 in memoria nel singolo processo. Socket.IO 4.8.3 risolve questo problema con l&#8217;adapter Redis (<code>@socket.io\/redis-adapter<\/code>), che sincronizza gli eventi tra tutte le istanze. Le stesse regole di sicurezza (auth, origin, rate limiting) si applicano, ma vengono implementate in middleware Socket.IO invece che nei gestori <code>ws<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Per la sicurezza specifica di Socket.IO, usa il middleware <code>io.use()<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ Socket.IO authentication middleware\nconst { Server } = require('socket.io');\nconst io = new Server(server, {\n  cors: {\n    origin: process.env.ALLOWED_ORIGINS.split(','),\n    methods: ['GET', 'POST'],\n    credentials: true\n  },\n  maxHttpBufferSize: 1e5  \/\/ 100KB max payload\n});\n\nio.use((socket, next) => {\n  const token = socket.handshake.auth.token;\n  if (!token) return next(new Error('Token assente'));\n\n  try {\n    const payload = jwt.verify(token, process.env.JWT_SECRET, {\n      algorithms: ['HS256'],\n      issuer: 'tuodominio.it',\n      audience: 'websocket-api'\n    });\n    socket.userId = payload.sub;\n    next();\n  } catch (err) {\n    next(new Error('Token non valido'));\n  }\n});<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"scansione-di-sicurezza-con-owasp-zap\">Scansione di sicurezza con OWASP ZAP<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">OWASP ZAP (Zed Attack Proxy) supporta il testing di sicurezza dei WebSocket dal 2024. Puoi configurarlo come proxy per le connessioni WebSocket e usarlo per iniettare payload malevoli, testare il rate limiting e verificare la corretta gestione degli errori. In un pipeline CI\/CD, integra i test ZAP automatizzati per verificare che ogni deploy mantenga la postura di sicurezza WebSocket.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"checklist-di-sicurezza-websocket\">Checklist di sicurezza WebSocket<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Controllo<\/th><th>Implementato<\/th><th>Note<\/th><\/tr><\/thead><tbody><tr><td>WSS (WebSocket su TLS)<\/td><td>Step 2-3<\/td><td>Obbligatorio in produzione<\/td><\/tr><tr><td>Autenticazione JWT sull&#8217;handshake<\/td><td>Step 4<\/td><td>Token con scadenza breve (1h)<\/td><\/tr><tr><td>Validazione header Origin<\/td><td>Step 5<\/td><td>Whitelist esplicita di origini<\/td><\/tr><tr><td>Rate limiting per connessione<\/td><td>Step 6<\/td><td>Max 10 upgrade\/IP\/15min<\/td><\/tr><tr><td>Rate limiting per messaggio<\/td><td>Step 6<\/td><td>Max 60 msg\/utente\/minuto<\/td><\/tr><tr><td>Validazione schema messaggi<\/td><td>Step 7<\/td><td>Tipo, lunghezza, struttura<\/td><\/tr><tr><td>maxPayload configurato<\/td><td>Step 3<\/td><td>64KB default<\/td><\/tr><tr><td>Heartbeat ping\/pong<\/td><td>Step 8<\/td><td>Check ogni 30 secondi<\/td><\/tr><tr><td>Limite connessioni globale<\/td><td>Step 9<\/td><td>Max 500 (configurabile)<\/td><\/tr><tr><td>Limite connessioni per utente<\/td><td>Step 9<\/td><td>Max 3 per userId<\/td><\/tr><tr><td>Logging sicuro (senza dati sensibili)<\/td><td>Step 10<\/td><td>IP anonimizzato, no token<\/td><\/tr><tr><td>Test di sicurezza automatizzati<\/td><td>Step 12<\/td><td>wscat + artillery<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"copertura-correlata\">Copertura correlata<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"approfondimenti-su-node-js-e-sicurezza\">Approfondimenti su Node.js e sicurezza<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/it\/autenticazione-jwt-nodejs\/\">Autenticazione JWT in Node.js: 12 Step<\/a> &#8211; come generare e validare token JWT per API REST<\/li>\n<li><a href=\"\/it\/rate-limiting-nodejs\/\">Rate Limiting in Node.js: API Sicura in 12 Step<\/a> &#8211; strategie complete di rate limiting per Express<\/li>\n<li><a href=\"\/it\/xss-prevention-nodejs\/\">XSS in Node.js: Prevenirlo in 12 Step<\/a> &#8211; sanitizzazione degli input e Content Security Policy<\/li>\n<li><a href=\"\/it\/oauth2-pkce-nodejs\/\">OAuth 2.0 e PKCE in Node.js: 12 Step in 30 Minuti<\/a> &#8211; flusso di autenticazione sicuro per SPA<\/li>\n<li><a href=\"\/it\/openssl-certificati-chiavi\/\">OpenSSL 3.5 LTS: Chiavi e Certificati in 12 Step<\/a> &#8211; generazione e gestione di certificati TLS<\/li>\n<li><a href=\"\/it\/owasp-top-10-nodejs-2026\/\">OWASP Top 10 2025 in Node.js: 10 Vulnerabilit\u00e0, 12 Difese<\/a> &#8211; panoramica completa delle vulnerabilit\u00e0 OWASP in Node.js<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"faq-sui-websocket-sicuri-in-node-js\">FAQ sui WebSocket sicuri in Node.js<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"devo-usare-ws-o-socket-io-per-la-sicurezza\">Devo usare <code>ws<\/code> o Socket.IO per la sicurezza?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Entrambe le librerie possono essere rese sicure. <code>ws@8.21.0<\/code> \u00e8 pi\u00f9 leggera e ti d\u00e0 controllo totale sull&#8217;implementazione: ideale quando hai requisiti di sicurezza specifici o vuoi ridurre la superficie di attacco al minimo. Socket.IO 4.8.3 aggiunge fallback (long-polling), rooms, broadcasting e adattatori Redis: perfetta per applicazioni che richiedono scalabilit\u00e0 orizzontale. Per la sicurezza, Socket.IO richiede attenzione alla configurazione CORS e al <code>maxHttpBufferSize<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"come-rinnovo-il-jwt-durante-una-connessione-websocket-di-lunga-durata\">Come rinnovo il JWT durante una connessione WebSocket di lunga durata?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Il pattern pi\u00f9 comune \u00e8 l&#8217;implementazione di un meccanismo di rinnovo lato client: il client monitora la scadenza del token (campo <code>exp<\/code> nel payload) e richiede un nuovo token tramite un endpoint HTTP REST circa 5 minuti prima della scadenza, senza chiudere la connessione WebSocket. Il server pu\u00f2 anche inviare un evento WebSocket al client (&#8220;token_expiring_soon&#8221;) per avvisarlo. In alternativa, il server verifica periodicamente la validit\u00e0 del token stored nel socket e chiude le connessioni scadute.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"il-rate-limiting-websocket-rompe-le-connessioni-legittime\">Il rate limiting WebSocket rompe le connessioni legittime?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Solo se i limiti sono configurati in modo troppo restrittivo. Per applicazioni di chat, 60 messaggi al minuto per utente \u00e8 generoso (1 messaggio al secondo). Per applicazioni real-time pi\u00f9 intensive (trading, gaming), aumenta il limite ma implementa sempre un massimale assoluto. \u00c8 importante distinguere il rate limiting per connessione (numero di connessioni aperte) dal rate limiting per messaggio (frequenza dei messaggi su una connessione): i due controlli si complementano.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-websocket-sono-vulnerabili-agli-attacchi-csrf\">I WebSocket sono vulnerabili agli attacchi CSRF?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">S\u00ec, attraverso il Cross-Site WebSocket Hijacking (CSWSH). I token CSRF tradizionali non funzionano sui WebSocket perch\u00e9 il handshake \u00e8 una richiesta GET (non un form POST). Le difese corrette sono: validazione dell&#8217;header Origin (step 5) e autenticazione tramite token esplicito (JWT nel query string o header, non in cookie) come descritto nello step 4. Questi due controlli insieme eliminano il rischio CSWSH.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"come-funziona-la-sicurezza-websocket-dietro-un-reverse-proxy-nginx\">Come funziona la sicurezza WebSocket dietro un reverse proxy Nginx?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Con Nginx come reverse proxy, il TLS viene terminato a livello Nginx (che parla <code>wss:\/\/<\/code> con i client) e il traffico viene inoltrato a Node.js in chiaro (<code>ws:\/\/<\/code>) sulla rete interna. Questo \u00e8 il pattern corretto per la produzione: Nginx gestisce TLS e load balancing, Node.js gestisce la logica applicativa. Aggiungi nel blocco Nginx: <code>proxy_http_version 1.1;<\/code>, <code>proxy_set_header Upgrade $http_upgrade;<\/code>, <code>proxy_set_header Connection \"upgrade\";<\/code>. Per il rate limiting delle connessioni, configura anche <code>limit_conn<\/code> in Nginx.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"qual-e-la-differenza-tra-ws-close-e-ws-terminate\">Qual \u00e8 la differenza tra <code>ws.close()<\/code> e <code>ws.terminate()<\/code>?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>ws.close(code, reason)<\/code> invia un frame di chiusura WebSocket al client (chiusura &#8220;educata&#8221;: il client riceve notifica e pu\u00f2 pulire le risorse) e attende che il client risponda con il proprio frame di chiusura. <code>ws.terminate()<\/code> chiude immediatamente la connessione TCP senza notifican il client: usalo per connessioni zombie (che non rispondono al ping), per client che violano il rate limit in modo aggressivo o in caso di errori critici. In caso di heartbeat timeout, <code>terminate()<\/code> \u00e8 la scelta corretta perch\u00e9 <code>close()<\/code> potrebbe non avere effetto su una connessione che non risponde.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"il-modulo-nativo-ws-di-node-js-supporta-websocket-sicuri\">Il modulo nativo <code>ws<\/code> di Node.js supporta WebSocket sicuri?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Node.js non ha un server WebSocket built-in: il modulo nativo gestisce solo HTTP\/HTTPS. Per WebSocket \u00e8 necessario un pacchetto esterno come <code>ws<\/code> o Socket.IO. La libreria <code>ws<\/code> si integra con il server HTTPS nativo di Node.js per creare WSS, come mostrato nello step 3. Node.js 22.x ha introdotto supporto sperimentale per <code>WebSocket<\/code> lato client nel runtime, ma non include un server WebSocket nativo.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"come-monitoro-le-connessioni-websocket-attive-in-produzione\">Come monitoro le connessioni WebSocket attive in produzione?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Esponi una route interna (non pubblica) che restituisce statistiche sulle connessioni attive: <code>wss.clients.size<\/code> per il conteggio globale, informazioni per userId dalla Map <code>userConnections<\/code>. Integra queste metriche con Prometheus (usando <code>prom-client<\/code>) per avere grafici storici e alert quando il numero di connessioni si avvicina al limite. Monitora anche la memoria del processo Node.js (<code>process.memoryUsage()<\/code>) e imposta alert su <code>heapUsed<\/code> per rilevare memory leak nelle strutture dati dei WebSocket.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Un&#8217;applicazione Node.js che usa WebSocket non autenticati \u00e8 come lasciare una porta sul retro aperta: chiunque pu\u00f2 entrare, restare quanto vuole e inviare qualsiasi messaggio. Secondo i dati del OWASP\u2026<\/p>\n","protected":false},"author":4,"featured_media":302,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-301","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-security"],"_links":{"self":[{"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/posts\/301","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/comments?post=301"}],"version-history":[{"count":0,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/posts\/301\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/media\/302"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/media?parent=301"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/categories?post=301"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/tags?post=301"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}