{"id":191,"date":"2026-06-17T08:00:00","date_gmt":"2026-06-17T08:00:00","guid":{"rendered":"https:\/\/shattered.io\/it\/2026\/06\/17\/owasp-top-10-nodejs-2026\/"},"modified":"2026-06-17T08:00:00","modified_gmt":"2026-06-17T08:00:00","slug":"owasp-top-10-nodejs-2026","status":"publish","type":"post","link":"https:\/\/shattered.io\/it\/2026\/06\/17\/owasp-top-10-nodejs-2026\/","title":{"rendered":"OWASP Top 10 2025 in Node.js: 10 Vulnerabilit\u00e0, 12 Difese [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">L&#8217;<a href=\"https:\/\/owasp.org\/www-project-top-ten\/\" target=\"_blank\" rel=\"noopener\"><strong>OWASP Top 10<\/strong><\/a> \u00e8 la lista delle 10 vulnerabilit\u00e0 web pi\u00f9 critiche aggiornata dall&#8217;Open Web Application Security Project. La versione 2025, la pi\u00f9 recente, cambia l&#8217;ordine rispetto al 2021: il Broken Access Control rimane al primo posto con un tasso di incidenza del 3,73% delle applicazioni testate, la Security Misconfiguration sale al secondo posto, e i Software Supply Chain Failures entrano con il pi\u00f9 alto tasso medio di incidenza del 5,19% e oltre 215.000 occurrenze rilevate. In questo tutorial implementiamo le difese per tutte e 10 le vulnerabilit\u00e0 in Node.js e Express con codice funzionante, step per step.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ogni sezione mostra il pattern vulnerabile, poi la correzione con il codice esatto da usare. Nessuna teoria senza codice: ogni vulnerabilit\u00e0 include un esempio realistico di come viene sfruttata e la contromisura specifica. Al termine del tutorial avrai un middleware stack Express production-ready che copre tutte e 10 le categorie OWASP 2025.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"owasp-top-10-2025-le-10-vulnerabilita-in-ordine-di-rischio\">OWASP Top 10 2025: Le 10 Vulnerabilit\u00e0 in Ordine di Rischio<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La versione 2025 dell&#8217;OWASP Top 10 introduce cambiamenti significativi rispetto al 2021. Due nuove categorie fanno il loro ingresso: <strong>Software Supply Chain Failures<\/strong> (A03) e <strong>Mishandling of Exceptional Conditions<\/strong> (A10). Il Server-Side Request Forgery (SSRF), che era A10 nel 2021 come categoria separata, viene ora assorbito in Broken Access Control e Insecure Design.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Ranking 2025<\/th><th>Categoria<\/th><th>Ranking 2021<\/th><th>Incidenza<\/th><th>Impatto<\/th><\/tr><\/thead><tbody><tr><td>A01:2025<\/td><td>Broken Access Control<\/td><td>A01:2021<\/td><td>3,73%<\/td><td>Critico<\/td><\/tr><tr><td>A02:2025<\/td><td>Security Misconfiguration<\/td><td>A05:2021<\/td><td>Alta<\/td><td>Alto<\/td><\/tr><tr><td>A03:2025<\/td><td>Software Supply Chain Failures<\/td><td>Nuova (5,19%)<\/td><td>5,19%<\/td><td>Critico<\/td><\/tr><tr><td>A04:2025<\/td><td>Cryptographic Failures<\/td><td>A02:2021<\/td><td>Alta<\/td><td>Critico<\/td><\/tr><tr><td>A05:2025<\/td><td>Injection<\/td><td>A03:2021<\/td><td>100% app test<\/td><td>Critico<\/td><\/tr><tr><td>A06:2025<\/td><td>Insecure Design<\/td><td>A04:2021<\/td><td>Media<\/td><td>Alto<\/td><\/tr><tr><td>A07:2025<\/td><td>Authentication Failures<\/td><td>A07:2021<\/td><td>Alta<\/td><td>Critico<\/td><\/tr><tr><td>A08:2025<\/td><td>Software and Data Integrity Failures<\/td><td>A08:2021<\/td><td>Media<\/td><td>Alto<\/td><\/tr><tr><td>A09:2025<\/td><td>Security Logging and Alerting Failures<\/td><td>A09:2021<\/td><td>Alta<\/td><td>Medio<\/td><\/tr><tr><td>A10:2025<\/td><td>Mishandling of Exceptional Conditions<\/td><td>Nuova (24 CWE)<\/td><td>24 CWE<\/td><td>Alto<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"prerequisiti-e-setup-del-progetto\">Prerequisiti e Setup del Progetto<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Prima di iniziare, verifica di avere installato l&#8217;ambiente di sviluppo corretto. Questo tutorial richiede Node.js 18+ e usa Express come framework web. I pacchetti di sicurezza che installeremo sono tutti manutentivi attivamente e aggiornati nel 2025-2026.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Requisito<\/th><th>Versione<\/th><th>Verifica<\/th><th>Scopo nel tutorial<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>18.0.0+<\/td><td><code>node --version<\/code><\/td><td>Runtime JavaScript<\/td><\/tr><tr><td>npm<\/td><td>9.0.0+<\/td><td><code>npm --version<\/code><\/td><td>Gestione pacchetti<\/td><\/tr><tr><td>Express<\/td><td>4.18+<\/td><td>Package.json<\/td><td>Framework web<\/td><\/tr><tr><td>helmet<\/td><td>7.x<\/td><td><code>npm list helmet<\/code><\/td><td>Header di sicurezza HTTP<\/td><\/tr><tr><td>express-validator<\/td><td>7.x<\/td><td><code>npm list express-validator<\/code><\/td><td>Validazione input<\/td><\/tr><tr><td>mysql2 o pg<\/td><td>Ultima<\/td><td><code>npm list mysql2<\/code><\/td><td>Query parametrizzate<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Crea il progetto e installa le dipendenze di sicurezza:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir owasp-nodejs-demo && cd owasp-nodejs-demo\nnpm init -y\nnpm install express helmet express-validator mysql2 pg jsonwebtoken bcrypt\nnpm install --save-dev nodemon\n\n# Verifica le versioni installate\nnpm list --depth=0<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-1-2-a01-broken-access-control-il-rischio-numero-uno\">Step 1-2: A01 Broken Access Control &#8211; Il Rischio Numero Uno<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Il Broken Access Control \u00e8 rimasto al primo posto nell&#8217;OWASP Top 10 sia nel 2021 che nel 2025, con un tasso di incidenza del 3,73% tra tutte le applicazioni analizzate. In Node.js, il pattern vulnerabile pi\u00f9 comune \u00e8 un&#8217;API che usa un parametro dell&#8217;URL per identificare la risorsa senza verificare che l&#8217;utente autenticato sia il proprietario di quella risorsa.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step 1: Identificare il pattern vulnerabile (IDOR &#8211; Insecure Direct Object Reference).<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ VULNERABILE: chiunque con un token valido pu\u00f2 leggere qualsiasi fattura\napp.get('\/api\/invoices\/:id', requireAuth, async (req, res) => {\n  const invoice = await db.invoice.findById(req.params.id);\n  if (!invoice) return res.status(404).json({ error: 'Non trovata' });\n  res.json(invoice); \/\/ ERRORE: non verifica che invoice.ownerId === req.user.id\n});\n\n\/\/ Attacco: un utente malintenzionato prova semplicemente\n\/\/ GET \/api\/invoices\/1, \/api\/invoices\/2, \/api\/invoices\/3 ecc.\n\/\/ e legge le fatture di tutti gli utenti<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step 2: Implementare l&#8217;autorizzazione a livello di oggetto con middleware riutilizzabili.<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ security\/access-control.js - Middleware di controllo accessi centralizzato\n\n\/**\n * Verifica che l'utente autenticato sia proprietario della risorsa.\n * @param {Function} loadResource - Funzione che carica la risorsa dato l'ID\n * @param {string} ownerField - Campo della risorsa che contiene l'ID del proprietario\n *\/\nfunction requireOwnership(loadResource, ownerField = 'userId') {\n  return async (req, res, next) => {\n    try {\n      const resource = await loadResource(req.params.id);\n      if (!resource) return res.status(404).json({ error: 'Risorsa non trovata' });\n\n      \/\/ Confronto stretto tra l'ID del proprietario e l'ID dell'utente autenticato\n      if (String(resource[ownerField]) !== String(req.user.id)) {\n        return res.status(403).json({ error: 'Accesso negato' });\n      }\n\n      req.resource = resource; \/\/ Disponibile per il next handler\n      next();\n    } catch (err) {\n      next(err);\n    }\n  };\n}\n\n\/**\n * Verifica che l'utente abbia un ruolo specifico.\n * @param {...string} roles - Ruoli ammessi\n *\/\nfunction requireRole(...roles) {\n  return (req, res, next) => {\n    if (!req.user || !roles.includes(req.user.role)) {\n      return res.status(403).json({ error: 'Autorizzazione insufficiente' });\n    }\n    next();\n  };\n}\n\nmodule.exports = { requireOwnership, requireRole };\n\n\/\/ Uso corretto\nconst { requireOwnership, requireRole } = require('.\/security\/access-control');\n\napp.get('\/api\/invoices\/:id',\n  requireAuth,\n  requireOwnership(id => db.invoice.findById(id), 'ownerId'),\n  (req, res) => {\n    res.json(req.resource); \/\/ Solo la risorsa dell'utente autenticato\n  }\n);\n\napp.delete('\/api\/users\/:id', requireAuth, requireRole('admin'), async (req, res) => {\n  await db.user.delete(req.params.id);\n  res.sendStatus(204);\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-3-4-a02-security-misconfiguration-header-e-configurazione-sicura\">Step 3-4: A02 Security Misconfiguration &#8211; Header e Configurazione Sicura<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La Security Misconfiguration ha scalato dal quinto al secondo posto nell&#8217;OWASP Top 10 2025. In Node.js ed Express, le configurazioni errate pi\u00f9 frequenti includono: header HTTP di sicurezza assenti, l&#8217;header <code>X-Powered-By: Express<\/code> che rivela il framework in uso, stack trace esposti nei messaggi di errore, variabili d&#8217;ambiente con segreti committate nel repository, e endpoint di debug accessibili in produzione.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step 3: Configurare Helmet.js con Content Security Policy.<\/strong> Helmet aggiunge automaticamente 11 header di sicurezza HTTP. La configurazione di default protegge da clickjacking, MIME sniffing, XSS e altri attacchi, ma la Content Security Policy va personalizzata per la tua applicazione.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ security\/helmet-config.js - Configurazione Helmet produzione\nconst helmet = require('helmet');\n\nfunction setupSecurityHeaders(app) {\n  \/\/ Rimuovi il fingerprint del framework\n  app.disable('x-powered-by');\n\n  app.use(helmet({\n    \/\/ Content Security Policy: previene XSS e injection di risorse esterne\n    contentSecurityPolicy: {\n      useDefaults: true,\n      directives: {\n        defaultSrc: [\"'self'\"],\n        scriptSrc: [\"'self'\"],\n        styleSrc: [\"'self'\", \"'unsafe-inline'\"], \/\/ Rimuovi unsafe-inline se possibile\n        imgSrc: [\"'self'\", 'data:', 'https:'],\n        connectSrc: [\"'self'\"],\n        fontSrc: [\"'self'\"],\n        objectSrc: [\"'none'\"],         \/\/ Disabilita plugin (Flash, Java)\n        mediaSrc: [\"'self'\"],\n        frameSrc: [\"'none'\"],\n        baseUri: [\"'self'\"],\n        frameAncestors: [\"'none'\"],    \/\/ Previene clickjacking\n        formAction: [\"'self'\"],\n        upgradeInsecureRequests: []\n      }\n    },\n    \/\/ HTTP Strict Transport Security: forza HTTPS per 1 anno\n    hsts: {\n      maxAge: 31536000,        \/\/ 1 anno in secondi\n      includeSubDomains: true,\n      preload: true\n    },\n    \/\/ Impedisce al browser di \"indovinare\" il MIME type\n    noSniff: true,\n    \/\/ Previene il rendering in iframe (clickjacking)\n    frameguard: { action: 'deny' },\n    \/\/ Cross-Origin policies per isolamento del processo\n    crossOriginOpenerPolicy: { policy: 'same-origin' },\n    crossOriginResourcePolicy: { policy: 'same-site' },\n    \/\/ Rimuovi header che rivelano info sul server\n    referrerPolicy: { policy: 'no-referrer' }\n  }));\n}\n\nmodule.exports = { setupSecurityHeaders };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step 4: Gestione sicura degli errori senza esporre dettagli interni.<\/strong> I messaggi di errore non devono mai contenere stack trace, query SQL, nomi di file interni o versioni delle librerie. Questa informazione aiuta un attaccante a identificare vulnerabilit\u00e0 specifiche.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ security\/error-handler.js - Gestore errori produzione\n\nconst IS_PROD = process.env.NODE_ENV === 'production';\n\n\/**\n * Middleware di gestione errori Express (4 parametri obbligatori).\n * Va registrato DOPO tutti gli altri middleware e route.\n *\/\nfunction secureErrorHandler(err, req, res, next) {\n  \/\/ Log dell'errore completo solo lato server\n  console.error({\n    timestamp: new Date().toISOString(),\n    method: req.method,\n    path: req.path,\n    error: err.message,\n    stack: err.stack,\n    user: req.user?.id\n  });\n\n  \/\/ Risposta generica al client: nessun dettaglio interno\n  const statusCode = err.statusCode || err.status || 500;\n  const clientMessage = IS_PROD\n    ? 'Si \u00e8 verificato un errore. Riprova pi\u00f9 tardi.'\n    : err.message; \/\/ Solo in sviluppo mostra il messaggio reale\n\n  res.status(statusCode).json({\n    error: clientMessage,\n    requestId: req.id \/\/ Solo per correlazione log, non rivela nulla\n  });\n}\n\nmodule.exports = { secureErrorHandler };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-5-a03-software-supply-chain-failures-difendere-le-dipendenze\">Step 5: A03 Software Supply Chain Failures &#8211; Difendere le Dipendenze<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I Software Supply Chain Failures sono la nuova categoria A03 nell&#8217;OWASP Top 10 2025, con il pi\u00f9 alto tasso medio di incidenza del 5,19% e oltre 215.000 occurrenze documentate nei dati contribuiti. Nel 2024-2025, diversi attacchi di supply chain hanno colpito l&#8217;ecosistema npm: pacchetti con script <code>postinstall<\/code> malevoli, typosquatting (nomi di pacchetti quasi identici a quelli legittimi), e dependency confusion (pacchetti interni sostituiti da quelli pubblici con stesso nome).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Verifica sicurezza dipendenze durante CI\/CD\n\/\/ package.json - Script di sicurezza\n\n{\n  \"scripts\": {\n    \"audit\": \"npm audit --audit-level=high\",\n    \"audit:fix\": \"npm audit fix\",\n    \"install:ci\": \"npm ci --ignore-scripts --omit=dev\",\n    \"check:licenses\": \"npx license-checker --onlyAllow 'MIT;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC'\"\n  }\n}\n\n\/\/ .npmrc - Configurazione npm sicura\n\/\/ audit=true\n\/\/ fund=false\n\/\/ ignore-scripts=false  # Valuta se ignorare gli script per dipendenze non fidate\n\/\/ save-exact=true       # Versioni esatte invece di range (^1.0.0)<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># Pipeline di sicurezza CI\/CD (GitHub Actions)\n# .github\/workflows\/security.yml\n\nname: Security Audit\non: [push, pull_request]\njobs:\n  audit:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\/checkout@v4\n      - uses: actions\/setup-node@v4\n        with:\n          node-version: '20'\n      - run: npm ci\n      - run: npm audit --audit-level=high\n      - run: npx snyk test  # Analisi avanzata vulnerabilit\u00e0 (opzionale)\n\n# Usa npm ci invece di npm install in produzione:\n# npm ci \u00e8 deterministico, usa package-lock.json e non modifica node_modules<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Misure aggiuntive per la sicurezza della supply chain: verifica l&#8217;integrit\u00e0 dei pacchetti con npm audit e Snyk a ogni build; usa Dependabot o Renovate per aggiornamenti automatici delle dipendenze con patch di sicurezza; mantieni il lockfile (<code>package-lock.json<\/code>) nel repository; considera l&#8217;uso di private npm registry per i pacchetti interni, isolandoli dal namespace pubblico.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-6-a04-cryptographic-failures-proteggere-i-dati-sensibili\">Step 6: A04 Cryptographic Failures &#8211; Proteggere i Dati Sensibili<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I Cryptographic Failures (ex &#8220;Sensitive Data Exposure&#8221; nel 2021) riguardano l&#8217;uso di algoritmi crittografici deboli, la mancata cifratura di dati sensibili, e la generazione di token con generatori non crittograficamente sicuri. In Node.js, l&#8217;errore pi\u00f9 comune \u00e8 usare <code>Math.random()<\/code> per generare token di sicurezza: produce numeri pseudo-casuali non sicuri, predicibili da un attaccante che analizza l&#8217;output del generatore.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ security\/crypto-utils.js - Utilit\u00e0 crittografiche sicure\nconst crypto = require('crypto');\nconst bcrypt = require('bcrypt');\n\n\/\/ VULNERABILE: Math.random() non \u00e8 crittograficamente sicuro\n\/\/ const token = Math.random().toString(36).slice(2); \/\/ NON USARE\n\n\/**\n * Genera un token crittograficamente sicuro.\n * @param {number} bytes - Lunghezza in byte (default 32 = 256 bit)\n * @returns {string} Token hex sicuro (64 caratteri per 32 byte)\n *\/\nfunction generateSecureToken(bytes = 32) {\n  return crypto.randomBytes(bytes).toString('hex');\n}\n\n\/**\n * Genera un OTP numerico a 6 cifre crittograficamente sicuro.\n *\/\nfunction generateSecureOTP() {\n  const randomValue = crypto.randomInt(0, 1000000);\n  return randomValue.toString().padStart(6, '0');\n}\n\n\/**\n * Hash sicuro della password con bcrypt (costo 12).\n * Il costo 12 richiede circa 250ms su hardware moderno: sufficiente per limitare brute force.\n *\/\nasync function hashPassword(password) {\n  const BCRYPT_ROUNDS = 12;\n  return bcrypt.hash(password, BCRYPT_ROUNDS);\n}\n\nasync function verifyPassword(password, hash) {\n  return bcrypt.compare(password, hash);\n}\n\n\/**\n * Confronto sicuro di stringhe (timing-safe): previene timing attacks.\n * NON usare === per confrontare token o hash.\n *\/\nfunction safeCompare(a, b) {\n  const bufA = Buffer.from(String(a));\n  const bufB = Buffer.from(String(b));\n  if (bufA.length !== bufB.length) return false;\n  return crypto.timingSafeEqual(bufA, bufB);\n}\n\nmodule.exports = { generateSecureToken, generateSecureOTP, hashPassword, verifyPassword, safeCompare };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-7-a05-injection-sql-injection-e-prevenzione\">Step 7: A05 Injection &#8211; SQL Injection e Prevenzione<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;Injection scende dal terzo al quinto posto nell&#8217;OWASP Top 10 2025, ma rimane il tipo di vulnerabilit\u00e0 presente nel 100% delle applicazioni testate nei dati contribuiti. La SQL injection \u00e8 il caso pi\u00f9 noto: concatenare input dell&#8217;utente in una query SQL permette a un attaccante di modificare la logica della query, aggirare l&#8217;autenticazione, leggere tabelle arbitrarie o eliminare dati. In Node.js, la difesa \u00e8 semplice ma richiede disciplina: usare sempre query parametrizzate.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ VULNERABILE: concatenazione di stringhe in SQL = SQL Injection\napp.get('\/users', async (req, res) => {\n  const email = req.query.email;\n  \/\/ Se email = \"' OR '1'='1\", restituisce TUTTI gli utenti\n  const sql = `SELECT * FROM users WHERE email = '${email}'`;\n  const [rows] = await pool.query(sql);\n  res.json(rows);\n});\n\n\/\/ Attacco classico:\n\/\/ GET \/users?email=' OR '1'='1'--\n\/\/ Restituisce tutti gli utenti, bypassando il filtro<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ CORRETTO con mysql2: query parametrizzata con placeholder ?\nconst mysql = require('mysql2\/promise');\nconst pool = mysql.createPool({\n  host: process.env.DB_HOST,\n  user: process.env.DB_USER,\n  password: process.env.DB_PASS,\n  database: process.env.DB_NAME,\n  waitForConnections: true,\n  connectionLimit: 10\n});\n\napp.get('\/users', requireAuth, async (req, res) => {\n  \/\/ Il placeholder ? e i valori separati prevengono SQL injection\n  const [rows] = await pool.execute(\n    'SELECT id, email, role FROM users WHERE email = ? AND status = ?',\n    [req.query.email, 'active'] \/\/ Valori passati separatamente, mai nel template SQL\n  );\n  res.json(rows);\n});\n\n\/\/ CORRETTO con pg (PostgreSQL): placeholder $1, $2 ecc.\nconst { Pool } = require('pg');\nconst pgPool = new Pool({ connectionString: process.env.DATABASE_URL });\n\napp.get('\/users', requireAuth, async (req, res) => {\n  const result = await pgPool.query(\n    'SELECT id, email, role FROM users WHERE email = $1 AND status = $2',\n    [req.query.email, 'active']\n  );\n  res.json(result.rows);\n});\n\n\/\/ Identificatori dinamici (ORDER BY, nome tabella): whitelist esplicita\nconst ALLOWED_SORT_COLUMNS = new Set(['created_at', 'email', 'name']);\n\napp.get('\/users\/list', requireAuth, async (req, res) => {\n  const sortBy = req.query.sort || 'created_at';\n  if (!ALLOWED_SORT_COLUMNS.has(sortBy)) {\n    return res.status(400).json({ error: 'Campo di ordinamento non valido' });\n  }\n  \/\/ Solo identificatori dalla whitelist - sicuro dalla SQL injection\n  const [rows] = await pool.execute(\n    `SELECT id, email FROM users ORDER BY ${sortBy} LIMIT ?`,\n    [parseInt(req.query.limit) || 20]\n  );\n  res.json(rows);\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Per ulteriori dettagli sulla prevenzione dell&#8217;injection in tutti i contesti (NoSQL, LDAP, OS command), consulta la <a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/SQL_Injection_Prevention_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener\">OWASP SQL Injection Prevention Cheat Sheet<\/a>. La regola fondamentale: non costruire mai query o comandi concatenando input non validato, qualunque sia il tipo di store (SQL, MongoDB, Redis, OS).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-8-a04-xss-e-prevenzione-sanitizzare-loutput\">Step 8: A04 XSS e Prevenzione &#8211; Sanitizzare l&#8217;Output<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Il Cross-Site Scripting (XSS) non \u00e8 una categoria separata nell&#8217;OWASP Top 10 2025 (era A03:2021 nella versione precedente come parte dell&#8217;Injection), ma rimane una delle vulnerabilit\u00e0 pi\u00f9 sfruttate nelle web application. In Node.js con Express, il rischio XSS sorge quando l&#8217;applicazione riflette input utente non sanitizzato direttamente nell&#8217;HTML della risposta, permettendo a un attaccante di iniettare script JavaScript che vengono eseguiti nel browser delle vittime.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ security\/xss-defense.js - Difesa da Cross-Site Scripting\nconst { body, validationResult, query } = require('express-validator');\nconst createDOMPurify = require('dompurify');\nconst { JSDOM } = require('jsdom');\n\n\/\/ Setup DOMPurify per ambiente Node.js (non ha il DOM nativo)\nconst window = new JSDOM('').window;\nconst DOMPurify = createDOMPurify(window);\n\n\/**\n * Sanitizza HTML accettato dall'utente (es. editor rich text).\n * Per input di solo testo, NON usare DOMPurify: usa escape e template literal semplici.\n *\/\nfunction sanitizeHtml(dirtyHtml) {\n  return DOMPurify.sanitize(dirtyHtml, {\n    USE_PROFILES: { html: true },\n    FORBID_TAGS: ['script', 'style', 'object', 'embed', 'form', 'input'],\n    FORBID_ATTR: ['onerror', 'onclick', 'onload', 'onmouseover', 'href']\n  });\n}\n\n\/\/ Middleware di validazione per un form commento\nconst validateComment = [\n  body('author').trim().isLength({ min: 1, max: 100 }).escape(), \/\/ .escape() per testo puro\n  body('content').trim().isLength({ min: 1, max: 5000 }),         \/\/ HTML: sanitize sotto\n  body('email').isEmail().normalizeEmail(),\n  query('page').optional().isInt({ min: 1, max: 100 })\n];\n\napp.post('\/comments',\n  requireAuth,\n  validateComment,\n  async (req, res) => {\n    const errors = validationResult(req);\n    if (!errors.isEmpty()) {\n      return res.status(400).json({ errors: errors.array() });\n    }\n\n    \/\/ Solo se il campo accetta HTML: sanitizza con DOMPurify\n    const safeContent = sanitizeHtml(req.body.content);\n\n    \/\/ Per testo puro: .escape() di express-validator o htmlspecialchars-equivalente\n    await db.comment.create({\n      author: req.body.author,    \/\/ Gi\u00e0 escapato da .escape()\n      content: safeContent,\n      userId: req.user.id\n    });\n\n    res.status(201).json({ success: true });\n  }\n);\n\nmodule.exports = { sanitizeHtml, validateComment };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Il Content-Security-Policy (CSP) configurato in Helmet al Step 3 fornisce un secondo livello di difesa: anche se del codice XSS raggiunge il browser, la CSP impedisce l&#8217;esecuzione di script inline non autorizzati e il caricamento di risorse da domini esterni. Per la guida completa sulla prevenzione XSS, consulta la <a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Cross_Site_Scripting_Prevention_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener\">OWASP XSS Prevention Cheat Sheet<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-9-a07-authentication-failures-jwt-sicuro-e-gestione-sessioni\">Step 9: A07 Authentication Failures &#8211; JWT Sicuro e Gestione Sessioni<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Gli Authentication Failures (A07:2025) coprono una serie di vulnerabilit\u00e0 legate alla gestione dell&#8217;identit\u00e0: uso di <code>jwt.decode()<\/code> invece di <code>jwt.verify()<\/code>, algoritmo JWT impostato su <code>none<\/code>, sessioni che non scadono, password deboli senza rate limiting, e mancata invalidazione dei token al logout. L&#8217;errore pi\u00f9 frequente in Node.js \u00e8 decodificare il JWT senza verificarne la firma.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ security\/auth-middleware.js - Autenticazione JWT sicura\nconst jwt = require('jsonwebtoken');\n\n\/\/ VULNERABILE: jwt.decode() non verifica la firma\n\/\/ const user = jwt.decode(token); \/\/ NON USARE IN PRODUZIONE\n\n\/**\n * Middleware di autenticazione JWT sicuro.\n * Verifica firma, algoritmo, scadenza, issuer e audience.\n *\/\nfunction requireAuth(req, res, next) {\n  try {\n    const authHeader = req.headers.authorization;\n    if (!authHeader || !authHeader.startsWith('Bearer ')) {\n      return res.status(401).json({ error: 'Token mancante' });\n    }\n\n    const token = authHeader.split(' ')[1];\n\n    req.user = jwt.verify(token, process.env.JWT_PUBLIC_KEY, {\n      algorithms: ['RS256'],          \/\/ Solo RS256: rifiuta 'none' e algoritmi deboli\n      issuer: process.env.JWT_ISSUER,\n      audience: process.env.JWT_AUDIENCE,\n      clockTolerance: 30              \/\/ 30 secondi di tolleranza per differenze di orologio\n    });\n\n    next();\n  } catch (err) {\n    if (err.name === 'TokenExpiredError') {\n      return res.status(401).json({ error: 'Token scaduto' });\n    }\n    return res.status(401).json({ error: 'Token non valido' });\n  }\n}\n\n\/**\n * Emissione di JWT sicuro con scadenza breve.\n *\/\nfunction issueAccessToken(userId, role) {\n  return jwt.sign(\n    {\n      sub: String(userId),\n      role,\n      jti: require('crypto').randomBytes(8).toString('hex') \/\/ JWT ID univoco per revoca\n    },\n    process.env.JWT_PRIVATE_KEY,\n    {\n      algorithm: 'RS256',\n      expiresIn: '15m',               \/\/ Access token breve: 15 minuti\n      issuer: process.env.JWT_ISSUER,\n      audience: process.env.JWT_AUDIENCE\n    }\n  );\n}\n\nmodule.exports = { requireAuth, issueAccessToken };<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Rate limiting per prevenire brute force su autenticazione\nconst rateLimit = require('express-rate-limit');\n\nconst authLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000, \/\/ Finestra di 15 minuti\n  max: 10,                    \/\/ Max 10 tentativi di login per finestra per IP\n  message: { error: 'Troppi tentativi. Riprova tra 15 minuti.' },\n  standardHeaders: true,\n  legacyHeaders: false,\n  \/\/ In produzione: usa un Redis store per persistenza cross-processo\n  \/\/ store: new RedisStore({ client: redisClient })\n});\n\napp.post('\/auth\/login', authLimiter, async (req, res) => {\n  const { email, password } = req.body;\n  const user = await db.user.findByEmail(email);\n\n  \/\/ Confronto costante nel tempo per prevenire timing attacks\n  const isValid = user && await bcrypt.compare(password, user.passwordHash);\n\n  if (!isValid) {\n    \/\/ Stessa risposta per email errata e password errata: no user enumeration\n    return res.status(401).json({ error: 'Credenziali non valide' });\n  }\n\n  const accessToken = issueAccessToken(user.id, user.role);\n  res.json({ accessToken });\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-10-a09-security-logging-e-a10-exceptional-conditions\">Step 10: A09 Security Logging e A10 Exceptional Conditions<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le Security Logging and Alerting Failures (A09:2025) indicano che l&#8217;applicazione non registra eventi di sicurezza critici: tentativi di accesso falliti, violazioni di autorizzazione, modifica di dati privilegiati, o bypass dei controlli di sicurezza. Senza questi log, un attaccante pu\u00f2 sondare l&#8217;applicazione per giorni senza essere rilevato.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il Mishandling of Exceptional Conditions (A10:2025) \u00e8 una categoria nuova che copre il comportamento dell&#8217;applicazione in caso di errore: se un servizio di policy o autenticazione \u00e8 temporaneamente non disponibile e l&#8217;applicazione &#8220;fallisce aperta&#8221; (concede l&#8217;accesso in caso di errore), un attaccante pu\u00f2 sfruttare questa condizione deliberatamente.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ security\/audit-logger.js - Log di sicurezza strutturato\n\nconst SECURITY_EVENTS = {\n  AUTH_FAILURE: 'auth.failure',\n  AUTH_SUCCESS: 'auth.success',\n  ACCESS_DENIED: 'access.denied',\n  ADMIN_ACTION: 'admin.action',\n  DATA_EXPORT: 'data.export',\n  PRIVILEGE_ESCALATION: 'privilege.escalation'\n};\n\n\/**\n * Registra un evento di sicurezza in formato strutturato.\n * In produzione, indirizzare a SIEM (Splunk, ELK, Datadog).\n *\/\nfunction auditLog(event, req, details = {}) {\n  const logEntry = {\n    timestamp: new Date().toISOString(),\n    event,\n    actor: req.user?.id || 'anonymous',\n    ip: req.ip || req.connection.remoteAddress,\n    userAgent: req.headers['user-agent'],\n    path: req.path,\n    method: req.method,\n    ...details\n  };\n  \/\/ In sviluppo: console. In produzione: logger strutturato (winston, pino)\n  console.log(JSON.stringify(logEntry));\n}\n\n\/\/ Middleware che registra tentativi di accesso non autorizzati\nfunction logAccessDenied(req, res, next) {\n  const originalJson = res.json.bind(res);\n  res.json = function(body) {\n    if (res.statusCode === 401 || res.statusCode === 403) {\n      auditLog(SECURITY_EVENTS.ACCESS_DENIED, req, {\n        status: res.statusCode,\n        targetResource: req.path\n      });\n    }\n    return originalJson(body);\n  };\n  next();\n}\n\nmodule.exports = { auditLog, logAccessDenied, SECURITY_EVENTS };<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ A10: Mishandling of Exceptional Conditions - Fail-Closed Pattern\n\n\/\/ VULNERABILE: fail-open - concede l'accesso in caso di errore del servizio di policy\napp.get('\/feature\/premium', async (req, res) => {\n  try {\n    const r = await fetch('https:\/\/policy-service\/check-access');\n    const { allowed } = await r.json();\n    if (allowed) return res.json({ feature: 'premium content' });\n    return res.status(403).json({ error: 'Non autorizzato' });\n  } catch (err) {\n    \/\/ ERRORE: se il servizio di policy \u00e8 gi\u00f9, concede l'accesso\n    return res.json({ feature: 'premium content' }); \/\/ VULNERABILE!\n  }\n});\n\n\/\/ CORRETTO: fail-closed - nega l'accesso in caso di errore del servizio di policy\napp.get('\/feature\/premium', requireAuth, async (req, res) => {\n  try {\n    const controller = new AbortController();\n    const timeout = setTimeout(() => controller.abort(), 3000); \/\/ 3 secondi timeout\n\n    const r = await fetch('https:\/\/policy-service\/check-access', {\n      signal: controller.signal,\n      headers: { 'X-User-Id': req.user.id }\n    });\n    clearTimeout(timeout);\n\n    const { allowed } = await r.json();\n    if (!allowed) return res.status(403).json({ error: 'Non autorizzato' });\n    res.json({ feature: 'premium content' });\n  } catch (err) {\n    \/\/ Fail-closed: in caso di errore del servizio di policy, NEGA l'accesso\n    auditLog('policy_service_failure', req, { error: err.message });\n    return res.status(503).json({ error: 'Servizio temporaneamente non disponibile' });\n  }\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-11-a08-integrita-dei-dati-e-a06-insecure-design\">Step 11: A08 Integrit\u00e0 dei Dati e A06 Insecure Design<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le Software and Data Integrity Failures (A08:2025) coprono casi in cui il codice o i dati vengono caricati e eseguiti senza verifica di integrit\u00e0. In Node.js, il caso pi\u00f9 comune \u00e8 accettare URL da un utente e caricare configurazione o moduli da quell&#8217;URL senza validazione. Il caso classico di SSRF (Server-Side Request Forgery) entra qui: un attaccante fornisce un URL interno (come <code>http:\/\/169.254.169.254\/<\/code>, il metadata server AWS) e il server lo raggiunge per conto dell&#8217;attaccante, esponendo credenziali cloud o servizi interni.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ security\/ssrf-defense.js - Protezione da Server-Side Request Forgery\n\nconst ALLOWED_PROTOCOLS = new Set(['http:', 'https:']);\nconst BLOCKED_HOSTS = new Set([\n  'localhost', '127.0.0.1', '0.0.0.0', '::1',\n  '169.254.169.254',  \/\/ AWS\/GCP metadata service\n  '100.100.100.200',  \/\/ Alibaba Cloud metadata service\n  'metadata.google.internal',\n  'fd00::ec2:254'     \/\/ IPv6 metadata\n]);\n\n\/\/ Blocca range di IP privati con regex semplice (produzione: usare libreria ip)\nfunction isPrivateRange(hostname) {\n  return (\n    \/^10\\.\/.test(hostname) ||          \/\/ 10.x.x.x\n    \/^172\\.(1[6-9]|2\\d|3[01])\\.\/.test(hostname) ||  \/\/ 172.16-31.x.x\n    \/^192\\.168\\.\/.test(hostname)        \/\/ 192.168.x.x\n  );\n}\n\n\/**\n * Valida un URL prima che il server effettui una richiesta esterna.\n * @param {string} rawUrl - URL fornito dall'utente\n * @returns {URL} Oggetto URL validato\n * @throws {Error} Se l'URL non \u00e8 sicuro\n *\/\nfunction validateExternalUrl(rawUrl) {\n  let url;\n  try {\n    url = new URL(rawUrl);\n  } catch {\n    throw Object.assign(new Error('URL non valido'), { statusCode: 400 });\n  }\n\n  if (!ALLOWED_PROTOCOLS.has(url.protocol)) {\n    throw Object.assign(new Error('Protocollo non consentito'), { statusCode: 400 });\n  }\n\n  if (BLOCKED_HOSTS.has(url.hostname) || isPrivateRange(url.hostname)) {\n    throw Object.assign(new Error('Destinazione non consentita'), { statusCode: 403 });\n  }\n\n  return url;\n}\n\n\/\/ Proxy sicuro con validazione SSRF\napp.get('\/proxy\/fetch', requireAuth, async (req, res) => {\n  try {\n    const safeUrl = validateExternalUrl(req.query.url);\n\n    const response = await fetch(safeUrl.toString(), {\n      redirect: 'manual', \/\/ Non seguire i redirect: un redirect potrebbe puntare a IP interni\n      headers: { 'User-Agent': 'MyApp\/1.0' },\n      signal: AbortSignal.timeout(5000)\n    });\n\n    if (response.status >= 300 && response.status < 400) {\n      return res.status(400).json({ error: 'Redirect non consentito' });\n    }\n\n    const contentType = response.headers.get('content-type') || 'text\/plain';\n    res.setHeader('Content-Type', 'text\/plain'); \/\/ Forza text\/plain: no XSS da contenuto remoto\n    res.send(await response.text());\n  } catch (err) {\n    res.status(err.statusCode || 500).json({ error: err.message });\n  }\n});\n\nmodule.exports = { validateExternalUrl };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">L'Insecure Design (A06:2025) riguarda invece i problemi a livello architetturale: meccanismi di sicurezza non integrati nella progettazione. Un esempio \u00e8 un sistema di reset password senza rate limiting lato server, che si affida solo al client per limitare i tentativi. La difesa qui \u00e8 progettuale: aggiungere il rate limiting e il cooldown lato server da subito, non come patch successiva.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-12-middleware-stack-completo-per-applicazioni-express-production\">Step 12: Middleware Stack Completo per Applicazioni Express Production<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">L'ultimo step assembla tutte le misure di sicurezza in un middleware stack Express completo. L'ordine dei middleware \u00e8 importante: la gestione degli errori va in coda, Helmet va prima di tutto il resto, e il rate limiter globale precede le route specifiche.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app.js - Applicazione Express con security stack completo OWASP Top 10\n\nconst express = require('express');\nconst rateLimit = require('express-rate-limit');\nconst { setupSecurityHeaders } = require('.\/security\/helmet-config');\nconst { secureErrorHandler } = require('.\/security\/error-handler');\nconst { requireAuth } = require('.\/security\/auth-middleware');\nconst { logAccessDenied } = require('.\/security\/audit-logger');\n\nconst app = express();\n\n\/\/ 1. HEADER DI SICUREZZA (A02: Security Misconfiguration)\nsetupSecurityHeaders(app);\n\n\/\/ 2. PARSING INPUT CON LIMITI\napp.use(express.json({ limit: '10kb' }));  \/\/ Limita payload JSON: previene DoS\napp.use(express.urlencoded({ extended: false, limit: '10kb' }));\n\n\/\/ 3. RATE LIMITING GLOBALE (A07: Authentication Failures + DoS)\napp.use(rateLimit({\n  windowMs: 15 * 60 * 1000,  \/\/ 15 minuti\n  max: 200,                    \/\/ Max 200 richieste per IP per finestra\n  standardHeaders: true,\n  message: { error: 'Troppe richieste. Riprova tra qualche minuto.' }\n}));\n\n\/\/ 4. LOG DI SICUREZZA (A09: Security Logging Failures)\napp.use(logAccessDenied);\n\n\/\/ 5. ROUTE APPLICAZIONE\napp.use('\/api\/auth', require('.\/routes\/auth'));      \/\/ Con rate limiter specifico per login\napp.use('\/api\/users', requireAuth, require('.\/routes\/users'));\napp.use('\/api\/invoices', requireAuth, require('.\/routes\/invoices'));\n\n\/\/ 6. GESTIONE ERRORI SICURA - DEVE ESSERE L'ULTIMO MIDDLEWARE (A02)\napp.use(secureErrorHandler);\n\n\/\/ Avvio sicuro\nconst PORT = parseInt(process.env.PORT) || 3000;\napp.listen(PORT, '127.0.0.1', () => {  \/\/ Bind solo su localhost, non 0.0.0.0 se dietro proxy\n  console.log(`[App] Server avviato su porta ${PORT} (NODE_ENV: ${process.env.NODE_ENV})`);\n});\n\nmodule.exports = app;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Per la guida completa sulle best practice di sicurezza Express in produzione, consulta la <a href=\"https:\/\/expressjs.com\/en\/advanced\/best-practice-security.html\" target=\"_blank\" rel=\"noopener\">documentazione ufficiale Express Security Best Practices<\/a> e la <a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Access_Control_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener\">OWASP Access Control Cheat Sheet<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"errori-comuni-e-insidie-nellimplementazione-della-sicurezza-owasp\">Errori Comuni e Insidie nell'Implementazione della Sicurezza OWASP<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Implementare le difese OWASP Top 10 non basta se si cadono in queste trappole frequenti nei progetti Node.js reali.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Trappola 1: Validare solo lato client.<\/strong> Le validazioni JavaScript nel browser non sono controlli di sicurezza. Un attaccante usa curl o Burp Suite per inviare dati arbitrari direttamente all'API. Ogni validazione di sicurezza va fatta lato server, con express-validator o simili.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Trappola 2: Controllare l'autenticazione ma non l'autorizzazione.<\/strong> Un middleware <code>requireAuth<\/code> che verifica solo la presenza del token JWT non \u00e8 sufficiente. Ogni endpoint che restituisce o modifica dati specifici di un utente deve verificare che l'utente autenticato sia il proprietario di quei dati.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Trappola 3: Usare <code>jwt.decode()<\/code> invece di <code>jwt.verify()<\/code>.<\/strong> <code>jwt.decode()<\/code> non verifica la firma del token. Un attaccante pu\u00f2 modificare il payload del JWT (ad esempio cambiare il ruolo da <code>user<\/code> a <code>admin<\/code>) senza invalidare la decodifica. Usare sempre <code>jwt.verify()<\/code> con la chiave pubblica e l'algoritmo esplicito.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Trappola 4: Configurare il CORS con <code>origin: '*'<\/code>.<\/strong> Un CORS aperto (<code>Access-Control-Allow-Origin: *<\/code>) con credenziali permesse \u00e8 una misconfiguration grave. Specificare esplicitamente i domini autorizzati: <code>origin: ['https:\/\/app.tuodominio.it', 'https:\/\/admin.tuodominio.it']<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Trappola 5: Registrare l'intera request\/response nei log.<\/strong> Log che includono header di autorizzazione, token, o corpo di richieste con dati personali creano un nuovo vettore di compromissione. Sanitizzare i log prima di scriverli: omettere <code>Authorization<\/code>, <code>Cookie<\/code>, password, e token.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Trappola 6: Nessun timeout sulle richieste esterne.<\/strong> Senza timeout, una richiesta a un servizio esterno che non risponde blocca il thread (o la Promise) indefinitamente. Usare sempre <code>AbortSignal.timeout(ms)<\/code> per le fetch esterne, o impostare <code>timeout<\/code> nel client HTTP.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"troubleshooting-8-problemi-frequenti-nella-sicurezza-node-js-express\">Troubleshooting: 8 Problemi Frequenti nella Sicurezza Node.js\/Express<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>1. Helmet blocca risorse legittime dell'applicazione (CSP violation).<\/strong> Il Content-Security-Policy \u00e8 troppo restrittivo e blocca font, immagini o script da CDN usati dall'app. Soluzione: controlla la console del browser per i messaggi \"Content Security Policy violation\", identifica la risorsa bloccata e aggiungi la direttiva corretta (es. <code>scriptSrc: [\"'self'\", 'cdn.jsdelivr.net']<\/code>). In sviluppo, usa il modo report-only prima di enforcement.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>2. Il rate limiter blocca utenti legittimi con molte richieste.<\/strong> Il limite globale \u00e8 troppo basso per alcune categorie di utenti (API client automatizzati, test di carico). Soluzione: differenzia il rate limit per tipo di endpoint. Le route di autenticazione possono avere un limite pi\u00f9 stretto (10 req\/15min), le API data un limite pi\u00f9 ampio (1000 req\/15min per IP autenticato).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>3. JsonWebTokenError: invalid signature dopo il deploy.<\/strong> La chiave privata\/pubblica RS256 \u00e8 cambiata tra ambienti o tra riavvii del processo. Soluzione: caricare le chiavi da variabili d'ambiente (<code>process.env.JWT_PRIVATE_KEY<\/code>) e verificare che siano identiche tra tutti i processi dell'applicazione. Se si usano variabili multilinea, usare <code>\\n<\/code> come separatore e sostituirle al caricamento.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>4. SQL injection possibile nonostante le query parametrizzate.<\/strong> Stai usando query parametrizzate per i valori ma concateni identificatori (nomi di colonne, tabelle, ORDER BY) dall'input utente. Soluzione: usa whitelist esplicite per tutti gli identificatori dinamici. I placeholder (<code>?<\/code> o <code>$1<\/code>) funzionano solo per i valori, non per i nomi di colonne o tabelle.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>5. express-validator non intercetta input malevoli.<\/strong> Hai aggiunto i validatori ma non chiami <code>validationResult(req)<\/code> nel handler. I validatori di express-validator raccolgono gli errori ma non bloccano la richiesta: devi controllare esplicitamente <code>if (!errors.isEmpty()) return res.status(400)...<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>6. Errori diversi per email non trovata vs password errata (user enumeration).<\/strong> Rispondendo con \"email non trovata\" vs \"password errata\", permetti a un attaccante di enumerare gli account esistenti. Usa sempre la stessa risposta per entrambi i casi: \"Credenziali non valide\". Il confronto della password deve avvenire anche quando l'utente non esiste (usa un hash fittizio) per evitare timing attacks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>7. Il log di sicurezza include dati sensibili (token, password).<\/strong> Molti logger JSON registrano l'intera request, inclusi gli header. Soluzione: configura il logger per omettere campi sensibili. Con Pino: <code>redact: ['req.headers.authorization', 'req.body.password', 'req.body.token']<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>8. SSRF possibile nonostante la validazione dell'hostname.<\/strong> Stai validando l'hostname dell'URL ma non verifichi l'IP a cui risolve il DNS. Un attaccante usa DNS rebinding: il dominio risolve a un IP pubblico durante la validazione e a un IP privato durante la fetch effettiva. Soluzione: risolvi il nome DNS prima di fare la fetch e verifica che l'IP risolto non sia in un range privato. In produzione, usa un egress firewall che blocca il traffico verso IP privati.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"checklist-di-sicurezza-owasp-per-applicazioni-node-js-in-produzione\">Checklist di Sicurezza OWASP per Applicazioni Node.js in Produzione<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Usa questa checklist prima di ogni deployment in produzione. Ogni punto corrisponde a una o pi\u00f9 categorie OWASP Top 10 2025.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Categoria OWASP<\/th><th>Controllo<\/th><th>Strumento\/Metodo<\/th><\/tr><\/thead><tbody><tr><td>A01: Broken Access Control<\/td><td>Ogni endpoint verifica ownership della risorsa<\/td><td>Code review, test di autorizzazione<\/td><\/tr><tr><td>A02: Security Misconfiguration<\/td><td>Helmet configurato, X-Powered-By rimosso, debug disabilitato<\/td><td><code>npm run helmet-check<\/code>, curl -I<\/td><\/tr><tr><td>A03: Supply Chain Failures<\/td><td>npm audit clean, lockfile committato, dipendenze aggiornate<\/td><td><code>npm audit --audit-level=high<\/code><\/td><\/tr><tr><td>A04: Cryptographic Failures<\/td><td>Nessun Math.random() per token, bcrypt per password, HTTPS forzato<\/td><td>Code review, HSTS check<\/td><\/tr><tr><td>A05: Injection<\/td><td>Tutte le query usano parametrizzazione, nessuna concatenazione SQL<\/td><td>Code review, sqlmap test<\/td><\/tr><tr><td>A06: Insecure Design<\/td><td>Rate limiting server-side su tutte le operazioni critiche<\/td><td>Load test, code review<\/td><\/tr><tr><td>A07: Auth Failures<\/td><td>jwt.verify() con algoritmo esplicito, scadenza breve, rate limit login<\/td><td>JWT debugger, test di scadenza<\/td><\/tr><tr><td>A08: Data Integrity<\/td><td>URL esterni validati, nessun SSRF, redirect disabilitati<\/td><td>SSRF test, burp suite<\/td><\/tr><tr><td>A09: Logging Failures<\/td><td>Log strutturati per auth failure, access denied, admin actions<\/td><td>Revisione log in staging<\/td><\/tr><tr><td>A10: Exceptional Conditions<\/td><td>Tutti i servizi esterni usano fail-closed, timeout impostati<\/td><td>Chaos test, service shutdown test<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"articoli-correlati\">Articoli Correlati<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Approfondisci i temi di sicurezza Node.js e crittografia trattati in questo tutorial:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/it\/rate-limiting-nodejs\/\">Rate Limiting in Node.js: API Sicura in 12 Step [2026]<\/a><\/li>\n<li><a href=\"\/it\/autenticazione-jwt-nodejs\/\">JWT Authentication in Node.js: 10 Step [2026]<\/a><\/li>\n<li><a href=\"\/it\/protezione-csrf-nodejs\/\">Protezione CSRF in Node.js: 12 Step [2026]<\/a><\/li>\n<li><a href=\"\/it\/hashing-password-bcrypt-nodejs\/\">bcrypt Password Hashing in Node.js: 11 Step [2026]<\/a><\/li>\n<li><a href=\"\/it\/burp-suite-tutorial\/\">Burp Suite: Test di Sicurezza Web in 12 Step [2026]<\/a><\/li>\n<li><a href=\"\/security\/\">Sicurezza: Guida alle Best Practice di Cybersecurity<\/a><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"domande-frequenti-sullowasp-top-10-in-node-js\">Domande Frequenti sull'OWASP Top 10 in Node.js<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>L'OWASP Top 10 2025 \u00e8 diverso dal 2021?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Si, in modo significativo. La versione 2025 introduce due nuove categorie: Software Supply Chain Failures (A03, che riflette l'aumento degli attacchi alla catena di fornitura software) e Mishandling of Exceptional Conditions (A10, con 24 CWE correlati). Il SSRF, che era una categoria separata nel 2021 (A10), viene distribuito tra Broken Access Control e Insecure Design nel 2025.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Come posso testare la mia applicazione Node.js per le vulnerabilit\u00e0 OWASP?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Usa una combinazione di strumenti: Burp Suite Community per test manuali di SQL injection e XSS, <code>npm audit<\/code> per le vulnerabilit\u00e0 nelle dipendenze, OWASP ZAP per scan automatizzati delle applicazioni web, e sqlmap per test approfonditi di SQL injection. Per il codice sorgente, strumenti di SAST (Static Application Security Testing) come Semgrep con le regole OWASP rilevano pattern vulnerabili durante la code review.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Il rate limiting \u00e8 sufficiente per prevenire gli attacchi di brute force?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il rate limiting \u00e8 necessario ma non sufficiente da solo. Un attaccante distribuito che usa migliaia di IP diversi (botnet) pu\u00f2 aggirare i limiti per IP. Le misure complementari includono: CAPTCHA dopo N tentativi falliti, blocco temporaneo dell'account dopo 5-10 tentativi, notifica all'utente di tentativi sospetti, e monitoraggio delle anomalie di accesso con alerting.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Devo usare un ORM per prevenire la SQL injection?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Un ORM come Sequelize, Prisma o TypeORM usa query parametrizzate di default, riducendo il rischio di SQL injection. Ma non elimina completamente il rischio: ORM che supportano query raw (<code>sequelize.query()<\/code>, <code>prisma.$queryRaw<\/code>) possono essere vulnerabili se si concatenano input utente. La regola rimane: mai concatenare input utente in query SQL, ORM o no.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Helmet.js \u00e8 sufficiente per la sicurezza degli header HTTP?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Helmet configura gli header di sicurezza pi\u00f9 importanti in modo rapido. Ma la Content Security Policy va personalizzata per la tua applicazione specifica: i default di Helmet potrebbero essere troppo restrittivi (bloccando risorse legittime) o insufficienti per scenari particolari. Verifica sempre la CSP con il report-only mode prima di abilitarla in enforcement.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Come gestisco i segreti dell'applicazione (chiavi API, password DB) in Node.js?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Non committare mai segreti nel repository. Usa variabili d'ambiente caricate da <code>.env<\/code> in sviluppo (con il file <code>.env<\/code> nel <code>.gitignore<\/code>) e da secret manager in produzione (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager, Kubernetes Secrets). Verifica periodicamente il repository con strumenti come <code>git-secrets<\/code> o <code>trufflehog<\/code> per rilevare segreti committati accidentalmente.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Quanto spesso viene aggiornato l'OWASP Top 10?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">L'OWASP Top 10 viene aggiornato ogni 3-4 anni, con versioni significative nel 2013, 2017, 2021, e ora 2025. Tra un'edizione e l'altra, OWASP pubblica cheat sheet e guide aggiornate pi\u00f9 frequentemente. Per rimanere aggiornato, segui il blog OWASP e iscriviti alla mailing list del progetto.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>L&#8217;OWASP Top 10 \u00e8 la lista delle 10 vulnerabilit\u00e0 web pi\u00f9 critiche aggiornata dall&#8217;Open Web Application Security Project. La versione 2025, la pi\u00f9 recente, cambia l&#8217;ordine rispetto al 2021: il\u2026<\/p>\n","protected":false},"author":4,"featured_media":192,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-191","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\/191","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=191"}],"version-history":[{"count":0,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/posts\/191\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/media\/192"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/media?parent=191"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/categories?post=191"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/tags?post=191"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}