{"id":216,"date":"2026-06-18T08:00:00","date_gmt":"2026-06-18T08:00:00","guid":{"rendered":"https:\/\/shattered.io\/it\/2026\/06\/18\/validazione-input-nodejs\/"},"modified":"2026-06-18T08:00:00","modified_gmt":"2026-06-18T08:00:00","slug":"validazione-input-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/it\/2026\/06\/18\/validazione-input-nodejs\/","title":{"rendered":"Validazione Input in Node.js: Zod, Joi e express-validator in 12 Step [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">La validazione degli input in Node.js non \u00e8 un dettaglio di implementazione: \u00e8 la prima linea di difesa contro SQL injection, XSS, command injection e una dozzina di altri attacchi classificati da OWASP come A03:2021. Un form non validato pu\u00f2 esporre il tuo database in meno di 60 secondi. Questa guida mostra come usare Zod 4.4.3 (201 milioni di download settimanali su npm), Joi 18.2.3 (23 milioni) ed express-validator 7.3.2 (2 milioni) per costruire un&#8217;API Express sicura in 12 step concreti, con esempi di codice pronti per la produzione.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"perche-la-validazione-degli-input-e-critica-nel-2026\">Perch\u00e9 la validazione degli input \u00e8 critica nel 2026<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OWASP Top 10 2021 posiziona l&#8217;injection al terzo posto (A03:2021), con SQL injection e XSS che restano le vulnerabilit\u00e0 pi\u00f9 sfruttate nelle applicazioni web. Il rilascio di sicurezza di Node.js del 24 marzo 2026 ha corretto 8 CVE sulle release attive v20.x, v22.x e v24.x, di cui 2 ad alta gravit\u00e0, 4 a gravit\u00e0 media e 2 a bassa gravit\u00e0. Il rilascio di sicurezza di gennaio 2026 aveva gi\u00e0 corretto 3 problemi di alta gravit\u00e0. Questo ritmo di patch conferma che anche il runtime stesso \u00e8 un vettore di attacco attivo, non solo il codice applicativo.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Express 5.2.1 non filtra automaticamente nessun dato in ingresso: tutto ci\u00f2 che arriva in <code>req.body<\/code>, <code>req.params<\/code> e <code>req.query<\/code> \u00e8 una stringa non attendibile senza validazione esplicita. Un attaccante che invia <code>{\"isAdmin\": true}<\/code> insieme a dati legittimi pu\u00f2 scalare privilegi se il controller non rimuove i campi non attesi. La validazione previene questo scenario prima ancora che i dati raggiungano la logica di business.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il vantaggio delle librerie di validazione rispetto al codice manuale \u00e8 la standardizzazione degli errori, la composizione degli schemi e la sanitizzazione integrata. Tre librerie coprono quasi ogni caso d&#8217;uso con API diverse e punti di forza complementari, che imparerai a confrontare e scegliere in questa guida.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"prerequisiti\">Prerequisiti<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Requisito<\/th><th>Versione<\/th><th>Note<\/th><\/tr><\/thead><tbody><tr><td>Node.js LTS<\/td><td>v24.17.0 (Krypton, rilasciato 17 giugno 2026)<\/td><td>Compatibile anche con v20.x e v22.x<\/td><\/tr><tr><td>npm<\/td><td>11.13.0<\/td><td>Incluso con Node.js v24<\/td><\/tr><tr><td>Express<\/td><td>5.2.1<\/td><td>Versione stabile attuale<\/td><\/tr><tr><td>express-validator<\/td><td>7.3.2<\/td><td>Middleware nativo Express<\/td><\/tr><tr><td>Joi<\/td><td>18.2.3<\/td><td>Schema validation enterprise<\/td><\/tr><tr><td>Zod<\/td><td>4.4.3<\/td><td>TypeScript-first, 201M download\/settimana<\/td><\/tr><tr><td>TypeScript (opzionale)<\/td><td>5.8.3<\/td><td>Richiesto per la type inference di Zod<\/td><\/tr><tr><td>Conoscenze<\/td><td>JavaScript ES2022, REST API, HTTP<\/td><td>Async\/await, destructuring<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-1-creazione-del-progetto-express\">Step 1: Creazione del progetto Express<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Crea una directory pulita e inizializza il progetto con le dipendenze principali. Express 5 include il supporto nativo alle promise nelle route handler, eliminando la necessit\u00e0 di <code>express-async-errors<\/code>. I middleware si registrano in ordine: prima JSON parsing, poi le route, infine il gestore di errori globale.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir node-validation-demo && cd node-validation-demo\nnpm init -y\n\n# Dipendenze produzione\nnpm install express@5.2.1 \\\n  express-validator@7.3.2 \\\n  joi@18.2.3 \\\n  zod@4.4.3 \\\n  sanitize-html@2.17.5 \\\n  mysql2@3.11.0 \\\n  multer@1.4.5-lts.2 \\\n  file-type@19.0.0\n\n# Dipendenze sviluppo\nnpm install --save-dev \\\n  typescript@5.8.3 \\\n  ts-node@10.9.2 \\\n  @types\/node \\\n  @types\/express \\\n  @types\/sanitize-html \\\n  jest@29 \\\n  supertest@7 \\\n  @types\/supertest<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Crea il file principale dell&#8217;applicazione con i middleware di sicurezza di base. Il limite di 10 KB sul body previene gli attacchi di tipo body-flooding. La configurazione <code>trust proxy<\/code> \u00e8 necessaria se l&#8217;app gira dietro Nginx o un load balancer per ottenere l&#8217;IP reale del client.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app.js\nconst express = require('express');\nconst app = express();\n\n\/\/ Parsing del body con limite di dimensione\napp.use(express.json({ limit: '10kb' }));\napp.use(express.urlencoded({ extended: true, limit: '10kb' }));\n\n\/\/ Gestione errori globale (Express 5: gestisce anche errori async)\napp.use((err, req, res, next) => {\n  if (err.type === 'entity.too.large') {\n    return res.status(413).json({ success: false, message: 'Payload troppo grande' });\n  }\n  console.error(err.stack);\n  res.status(500).json({ success: false, message: 'Errore interno del server' });\n});\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => console.log(`Server attivo su porta ${PORT}`));\n\nmodule.exports = app;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-2-prima-validazione-con-express-validator\">Step 2: Prima validazione con express-validator<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">express-validator 7.3.2 offre un&#8217;API a catena direttamente nelle route Express. Le funzioni <code>body()<\/code>, <code>param()<\/code> e <code>query()<\/code> coprono tutte le sorgenti di input. Ogni chiamata alla catena aggiunge una regola di validazione; la funzione <code>validationResult(req)<\/code> raccoglie tutti gli errori accumulati a fine catena.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ routes\/users.js\nconst express = require('express');\nconst { body, param, query, validationResult } = require('express-validator');\nconst router = express.Router();\n\n\/\/ Middleware riutilizzabile: raccoglie errori e risponde con 422\nconst handleValidation = (req, res, next) => {\n  const errors = validationResult(req);\n  if (!errors.isEmpty()) {\n    return res.status(422).json({\n      success: false,\n      errors: errors.array().map(e => ({\n        field: e.path,\n        message: e.msg,\n        received: e.value\n      }))\n    });\n  }\n  next();\n};\n\n\/\/ Route di registrazione con validazione completa\nrouter.post(\n  '\/register',\n  [\n    body('username')\n      .trim()\n      .isLength({ min: 3, max: 30 })\n      .withMessage('Username: da 3 a 30 caratteri')\n      .matches(\/^[a-zA-Z0-9_]+$\/)\n      .withMessage('Username: solo lettere, numeri e underscore')\n      .toLowerCase(),\n\n    body('email')\n      .trim()\n      .normalizeEmail()\n      .isEmail()\n      .withMessage('Indirizzo email non valido'),\n\n    body('password')\n      .isLength({ min: 12 })\n      .withMessage('Password: minimo 12 caratteri')\n      .matches(\/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])\/)\n      .withMessage('Richiesta: maiuscola, minuscola, numero e simbolo'),\n\n    body('age')\n      .optional()\n      .isInt({ min: 13, max: 120 })\n      .withMessage('Et\u00e0: valore tra 13 e 120')\n      .toInt(),\n\n    body('website')\n      .optional()\n      .isURL({ protocols: ['https'], require_protocol: true })\n      .withMessage('URL deve usare HTTPS'),\n  ],\n  handleValidation,\n  async (req, res) => {\n    const { username, email, age, website } = req.body;\n    \/\/ I dati sono stati validati e sanitizzati: sicuri da usare\n    res.status(201).json({\n      success: true,\n      data: { username, email, age, website }\n    });\n  }\n);\n\nmodule.exports = router;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Il middleware <code>handleValidation<\/code> centralizza la gestione degli errori. Questo pattern elimina la duplicazione del codice di controllo in ogni route. La risposta 422 (Unprocessable Entity) \u00e8 pi\u00f9 semanticamente corretta di 400 (Bad Request) per errori di validazione: il server ha capito la richiesta, ma i dati non sono processabili.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-3-sanitizzazione-integrata-con-express-validator\">Step 3: Sanitizzazione integrata con express-validator<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La validazione dice se un dato \u00e8 accettabile. La sanitizzazione trasforma i dati in una forma sicura prima che raggiungano il database o vengano restituiti al client. express-validator include sanitizzatori integrati che operano nella stessa catena di validazione, senza bisogno di librerie aggiuntive per i casi pi\u00f9 comuni.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Sanitizzatore<\/th><th>Cosa fa<\/th><th>Quando usarlo<\/th><\/tr><\/thead><tbody><tr><td><code>.trim()<\/code><\/td><td>Rimuove spazi iniziali e finali<\/td><td>Tutti i campi stringa<\/td><\/tr><tr><td><code>.escape()<\/code><\/td><td>Converte <code>&lt; &gt; &amp; \" '<\/code> in entit\u00e0 HTML<\/td><td>Testo plain visualizzato in HTML<\/td><\/tr><tr><td><code>.normalizeEmail()<\/code><\/td><td>Lowercase, rimuove alias Gmail (+tag)<\/td><td>Campi email<\/td><\/tr><tr><td><code>.toInt()<\/code><\/td><td>Converte stringa in intero<\/td><td>ID, et\u00e0, quantit\u00e0<\/td><\/tr><tr><td><code>.toFloat()<\/code><\/td><td>Converte stringa in float<\/td><td>Prezzi, coordinate GPS<\/td><\/tr><tr><td><code>.toBoolean()<\/code><\/td><td>Converte in boolean<\/td><td>Flag, preferenze utente<\/td><\/tr><tr><td><code>.toLowerCase()<\/code><\/td><td>Tutto in minuscolo<\/td><td>Username, slug, codici<\/td><\/tr><tr><td><code>.stripLow()<\/code><\/td><td>Rimuove caratteri di controllo ASCII<\/td><td>Campi testo libero<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Attenzione critica:<\/strong> <code>.escape()<\/code> converte <code>&lt;b&gt;testo&lt;\/b&gt;<\/code> in <code>&amp;lt;b&amp;gt;testo&amp;lt;\/b&amp;gt;<\/code>, rompendo la formattazione HTML. Se permetti HTML ricco (editor WYSIWYG, commenti formattati), usa <code>sanitize-html<\/code> al posto di <code>.escape()<\/code>. Vedi Step 9 per la configurazione completa.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-4-schema-validation-con-joi\">Step 4: Schema validation con Joi<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Joi 18.2.3 definisce schemi di validazione come oggetti JavaScript separati dalle route. Questo approccio \u00e8 preferibile quando hai schemi complessi, dipendenze condizionali tra campi (<code>Joi.when()<\/code>) o quando vuoi riutilizzare lo stesso schema in pi\u00f9 parti dell&#8217;applicazione. Con 23 milioni di download settimanali, Joi \u00e8 la scelta dominante nei backend enterprise Node.js.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ schemas\/productSchema.js\nconst Joi = require('joi');\n\nconst productSchema = Joi.object({\n  name: Joi.string()\n    .trim()\n    .min(2)\n    .max(100)\n    .required()\n    .messages({\n      'string.min': 'Il nome deve avere almeno 2 caratteri',\n      'string.max': 'Il nome non pu\u00f2 superare 100 caratteri',\n      'any.required': 'Il nome \u00e8 obbligatorio'\n    }),\n\n  price: Joi.number()\n    .positive()\n    .precision(2)\n    .max(99999.99)\n    .required()\n    .messages({\n      'number.positive': 'Il prezzo deve essere positivo',\n      'number.max': 'Prezzo massimo: 99.999,99 \u20ac'\n    }),\n\n  category: Joi.string()\n    .valid('elettronica', 'abbigliamento', 'alimentari', 'sport', 'casa')\n    .required()\n    .messages({\n      'any.only': 'Categoria non valida. Valori accettati: elettronica, abbigliamento, alimentari, sport, casa'\n    }),\n\n  tags: Joi.array()\n    .items(Joi.string().trim().lowercase().max(30))\n    .max(10)\n    .unique()\n    .default([]),\n\n  description: Joi.string().trim().max(2000).optional().allow(''),\n\n  sku: Joi.string()\n    .uppercase()\n    .pattern(\/^[A-Z]{3}-\\d{6}$\/)\n    .required()\n    .messages({\n      'string.pattern.base': 'Formato SKU: XXX-000000 (3 lettere, trattino, 6 cifre)'\n    }),\n\n  \/\/ Validazione condizionale: se isDigital \u00e8 true, downloadUrl \u00e8 obbligatorio\n  isDigital: Joi.boolean().default(false),\n  downloadUrl: Joi.when('isDigital', {\n    is: true,\n    then: Joi.string().uri({ scheme: ['https'] }).required()\n      .messages({ 'any.required': 'URL download obbligatorio per prodotti digitali' }),\n    otherwise: Joi.forbidden()\n  })\n});\n\n\/\/ Middleware di validazione Joi\nconst validateProduct = (req, res, next) => {\n  const { error, value } = productSchema.validate(req.body, {\n    abortEarly: false,    \/\/ Raccoglie tutti gli errori, non solo il primo\n    stripUnknown: true,   \/\/ Rimuove campi extra (previene mass assignment)\n    convert: true         \/\/ Converte i tipi automaticamente\n  });\n\n  if (error) {\n    return res.status(422).json({\n      success: false,\n      errors: error.details.map(d => ({\n        field: d.path.join('.'),\n        message: d.message\n      }))\n    });\n  }\n\n  req.body = value; \/\/ Sostituisce il body con i dati validati\/sanitizzati\n  next();\n};\n\nmodule.exports = { productSchema, validateProduct };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;opzione <code>stripUnknown: true<\/code> \u00e8 la difesa pi\u00f9 importante contro gli attacchi di mass-assignment. Rimuove silenziosamente tutti i campi non dichiarati nello schema prima che raggiungano il controller. Senza di essa, un attaccante pu\u00f2 inviare <code>{\"name\": \"Prodotto\", \"isAdmin\": true}<\/code> e il campo <code>isAdmin<\/code> arriver\u00e0 intatto al controller e poi al database.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-5-typescript-first-validation-con-zod\">Step 5: TypeScript-first validation con Zod<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Zod 4.4.3 ha superato 201 milioni di download settimanali, diventando la libreria di validazione pi\u00f9 scaricata su npm. Il suo vantaggio principale rispetto a Joi \u00e8 l&#8217;inferenza automatica dei tipi TypeScript dagli schemi: definisci lo schema una volta e ottieni automaticamente il tipo corrispondente senza duplicazione di codice. Zod 4 introduce performance significativamente migliorate rispetto a Zod 3 e una sintassi pi\u00f9 compatta per i messaggi di errore.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ schemas\/orderSchema.ts\nimport { z } from 'zod';\n\nconst OrderItemSchema = z.object({\n  productId: z.string().uuid({ message: 'ID prodotto non valido' }),\n  quantity: z.number().int().positive().max(100, 'Quantit\u00e0 massima: 100'),\n  unitPrice: z.number().positive().max(10000, 'Prezzo unitario massimo: 10.000 \u20ac')\n});\n\nconst OrderSchema = z.object({\n  customerId: z.string().uuid({ message: 'ID cliente non valido' }),\n\n  items: z\n    .array(OrderItemSchema)\n    .min(1, \"L'ordine deve contenere almeno un articolo\")\n    .max(50, 'Massimo 50 articoli per ordine'),\n\n  shippingAddress: z.object({\n    street: z.string().trim().min(5).max(200),\n    city: z.string().trim().min(2).max(100),\n    postalCode: z.string().regex(\/^\\d{5}$\/, 'CAP italiano: 5 cifre'),\n    country: z.enum(['IT', 'DE', 'FR', 'ES', 'NL', 'BE']).default('IT')\n  }),\n\n  couponCode: z\n    .string()\n    .trim()\n    .toUpperCase()\n    .regex(\/^[A-Z0-9]{6,12}$\/, 'Codice coupon non valido')\n    .optional(),\n\n  paymentMethod: z.enum(['carta', 'paypal', 'bonifico', 'contrassegno'], {\n    message: 'Metodo di pagamento non supportato'\n  }),\n\n  notes: z.string().trim().max(500).optional()\n});\n\n\/\/ Inferenza automatica del tipo TypeScript\ntype Order = z.infer<typeof OrderSchema>;\ntype OrderItem = z.infer<typeof OrderItemSchema>;\n\n\/\/ Middleware Express con Zod\nimport { Request, Response, NextFunction } from 'express';\n\nconst validateOrder = (req: Request, res: Response, next: NextFunction): void => {\n  const result = OrderSchema.safeParse(req.body);\n\n  if (!result.success) {\n    res.status(422).json({\n      success: false,\n      errors: result.error.issues.map(issue => ({\n        field: issue.path.join('.'),\n        message: issue.message,\n        code: issue.code\n      }))\n    });\n    return;\n  }\n\n  req.body = result.data;\n  next();\n};\n\nexport { OrderSchema, Order, OrderItem, validateOrder };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Usa sempre <code>safeParse()<\/code> nelle API, mai <code>parse()<\/code>: <code>safeParse()<\/code> restituisce un oggetto con <code>success<\/code> booleano senza mai lanciare eccezioni, mentre <code>parse()<\/code> genera un <code>ZodError<\/code> non gestito se la validazione fallisce. Usa <code>parse()<\/code> solo nei test dove vuoi che l&#8217;errore interrompa il test stesso.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-6-confronto-tra-le-tre-librerie\">Step 6: Confronto tra le tre librerie<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La scelta tra express-validator, Joi e Zod dipende dal contesto del progetto, dalla presenza di TypeScript e dalla complessit\u00e0 degli schemi. Nessuna delle tre \u00e8 superiore in assoluto: hanno punti di forza diversi che le rendono adatte a scenari diversi.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Caratteristica<\/th><th>express-validator 7.3.2<\/th><th>Joi 18.2.3<\/th><th>Zod 4.4.3<\/th><\/tr><\/thead><tbody><tr><td>Download settimanali npm<\/td><td>2,04 milioni<\/td><td>23,2 milioni<\/td><td>201,6 milioni<\/td><\/tr><tr><td>TypeScript support<\/td><td>Parziale (tipi base)<\/td><td>Buono<\/td><td>Eccellente (nativo)<\/td><\/tr><tr><td>Inferenza tipi automatica<\/td><td>No<\/td><td>No<\/td><td>Si (<code>z.infer<\/code>)<\/td><\/tr><tr><td>Integrazione Express<\/td><td>Nativa (middleware)<\/td><td>Via factory middleware<\/td><td>Via factory middleware<\/td><\/tr><tr><td>Bundle size (minified)<\/td><td>~50 KB<\/td><td>~145 KB<\/td><td>~64 KB (v4)<\/td><\/tr><tr><td>Validazione condizionale<\/td><td>Limitata<\/td><td>Eccellente (<code>Joi.when<\/code>)<\/td><td>Buona (<code>z.discriminatedUnion<\/code>)<\/td><\/tr><tr><td>Validazione asincrona<\/td><td>Si (custom validator)<\/td><td>Si (<code>validateAsync<\/code>)<\/td><td>Si (<code>z.promise<\/code>)<\/td><\/tr><tr><td>Strip campi extra<\/td><td>No (automatico)<\/td><td>Si (<code>stripUnknown<\/code>)<\/td><td>Si (<code>strip()<\/code>, default)<\/td><\/tr><tr><td>Curva di apprendimento<\/td><td>Bassa<\/td><td>Media<\/td><td>Media<\/td><\/tr><tr><td>Migliore per<\/td><td>Prototipi Express, migrazione graduale<\/td><td>Backend enterprise, schemi complessi<\/td><td>Progetti TypeScript, full-stack<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-7-prevenzione-sql-injection-con-query-parametrizzate\">Step 7: Prevenzione SQL Injection con query parametrizzate<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La validazione degli input riduce la superficie di attacco rifiutando dati malformati. Ma non sostituisce le query parametrizzate: anche con input validato, un carattere legittimo come l&#8217;apostrofo nel cognome O&#8217;Brien pu\u00f2 causare SQL injection in una query concatenata. Usa sempre i placeholder del driver del database per separare codice SQL e dati utente.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ db\/queries.js\nconst mysql2 = require('mysql2\/promise');\n\nconst pool = mysql2.createPool({\n  host: process.env.DB_HOST,\n  user: process.env.DB_USER,\n  password: process.env.DB_PASSWORD,\n  database: process.env.DB_NAME,\n  waitForConnections: true,\n  connectionLimit: 10,\n  namedPlaceholders: true  \/\/ Permette l'uso di :nome invece di ?\n});\n\n\/\/ VULNERABILE - SQL injection possibile\nasync function getUserByEmailUnsafe(email) {\n  const query = `SELECT * FROM users WHERE email = '${email}'`;\n  \/\/ Un attaccante invia: ' OR '1'='1' --\n  \/\/ La query diventa: WHERE email = '' OR '1'='1' -- '\n  \/\/ Restituisce TUTTI gli utenti\n  const [rows] = await pool.execute(query);\n  return rows[0];\n}\n\n\/\/ SICURO - query parametrizzata con placeholder\nasync function getUserByEmail(email) {\n  const [rows] = await pool.execute(\n    'SELECT id, username, email, created_at FROM users WHERE email = ? AND active = 1',\n    [email]  \/\/ Il driver esegue l'escape automatico\n  );\n  return rows[0] || null;\n}\n\n\/\/ SICURO - ricerca con placeholder multipli\nasync function searchProducts(name, category, maxPrice) {\n  const [rows] = await pool.execute(\n    `SELECT id, name, price, category, sku\n     FROM products\n     WHERE name LIKE ? AND category = ? AND price <= ? AND active = 1\n     ORDER BY price ASC\n     LIMIT 50`,\n    [`%${name.replace(\/[%_]\/g, '\\\\$&#038;')}%`, category, maxPrice]\n    \/\/ Escape dei metacaratteri LIKE: % e _ vengono preceduti da \\\n  );\n  return rows;\n}\n\n\/\/ SICURO - INSERT con named placeholders\nasync function createProduct({ name, price, category, sku }) {\n  const [result] = await pool.execute(\n    `INSERT INTO products (name, price, category, sku, created_at)\n     VALUES (:name, :price, :category, :sku, NOW())`,\n    { name, price, category, sku }\n  );\n  return result.insertId;\n}\n\nmodule.exports = { getUserByEmail, searchProducts, createProduct };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Per PostgreSQL con il driver <code>pg<\/code>, usa <code>$1, $2, $3<\/code> come placeholder. Con Prisma o TypeORM, le query parametrizzate sono gestite automaticamente dall'ORM, ma devi comunque validare gli input prima che raggiungano l'ORM per prevenire operazioni non autorizzate come <code>where: {}<\/code> che in Prisma seleziona tutti i record.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-8-middleware-di-validazione-centralizzato\">Step 8: Middleware di validazione centralizzato<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Un'architettura scalabile separa gli schemi di validazione dalla logica delle route. Il pattern factory crea middleware riutilizzabili a partire dagli schemi Zod o Joi. Ogni route riceve la validazione appropriata con una sola riga di codice, senza ripetere la gestione degli errori.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ middleware\/validateSchema.js\nconst { z } = require('zod');\n\n\/\/ Factory: crea un middleware che valida req.body con lo schema Zod fornito\nconst validateBody = (schema) => (req, res, next) => {\n  const result = schema.safeParse(req.body);\n  if (!result.success) {\n    return res.status(422).json({\n      success: false,\n      errors: result.error.issues.map(i => ({\n        field: i.path.join('.'),\n        message: i.message\n      }))\n    });\n  }\n  req.body = result.data;\n  next();\n};\n\n\/\/ Factory: valida req.params\nconst validateParams = (schema) => (req, res, next) => {\n  const result = schema.safeParse(req.params);\n  if (!result.success) {\n    return res.status(400).json({\n      success: false,\n      errors: result.error.issues\n    });\n  }\n  req.params = result.data;\n  next();\n};\n\n\/\/ Factory: valida req.query con coercizione dei tipi (stringa -> numero)\nconst validateQuery = (schema) => (req, res, next) => {\n  const result = schema.safeParse(req.query);\n  if (!result.success) {\n    return res.status(400).json({\n      success: false,\n      errors: result.error.issues\n    });\n  }\n  req.query = result.data;\n  next();\n};\n\n\/\/ Schemi esempio\nconst IdParamSchema = z.object({\n  id: z.string().uuid({ message: 'ID non valido: deve essere un UUID' })\n});\n\nconst PaginationSchema = z.object({\n  q: z.string().trim().min(2).max(100).optional(),\n  page: z.coerce.number().int().positive().default(1),\n  limit: z.coerce.number().int().positive().max(100).default(20),\n  sort: z.enum(['asc', 'desc']).default('desc')\n});\n\n\/\/ Utilizzo nelle route\n\/\/ router.get('\/products', validateQuery(PaginationSchema), listProducts);\n\/\/ router.get('\/products\/:id', validateParams(IdParamSchema), getProduct);\n\/\/ router.put('\/products\/:id', validateParams(IdParamSchema), validateBody(ProductSchema), updateProduct);\n\nmodule.exports = { validateBody, validateParams, validateQuery, IdParamSchema, PaginationSchema };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-9-prevenzione-xss-con-sanitize-html\">Step 9: Prevenzione XSS con sanitize-html<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Quando la tua applicazione deve accettare HTML ricco da parte degli utenti (editor WYSIWYG, commenti formattati, descrizioni prodotti), <code>sanitize-html<\/code> 2.17.5 (9,7 milioni di download settimanali su npm) rimuove selettivamente tag e attributi pericolosi mantenendo la formattazione sicura. La configurazione con whitelist esplicita \u00e8 la strategia corretta: nega tutto per default e permetti solo ci\u00f2 che \u00e8 necessario.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ utils\/sanitize.js\nconst sanitizeHtml = require('sanitize-html');\n\n\/\/ Per commenti utente (permissiva)\nconst commentOptions = {\n  allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li', 'code'],\n  allowedAttributes: {\n    'a': ['href', 'title']\n  },\n  allowedSchemes: ['https', 'mailto'],\n  \/\/ Blocca javascript: nei link, forza rel=\"noopener\"\n  transformTags: {\n    'a': (tagName, attribs) => {\n      const href = attribs.href || '';\n      if (href.startsWith('javascript:') || href.startsWith('data:')) {\n        return { tagName: 'span', attribs: {} };\n      }\n      return {\n        tagName: 'a',\n        attribs: {\n          href: attribs.href,\n          rel: 'noopener noreferrer',\n          target: '_blank'\n        }\n      };\n    }\n  }\n};\n\n\/\/ Per contenuto editoriale (pi\u00f9 permissiva)\nconst articleOptions = {\n  allowedTags: [\n    'h2', 'h3', 'h4', 'p', 'br', 'b', 'i', 'strong', 'em',\n    'ul', 'ol', 'li', 'blockquote', 'code', 'pre', 'a', 'table',\n    'thead', 'tbody', 'tr', 'th', 'td'\n  ],\n  allowedAttributes: {\n    'a': ['href'],\n    'code': ['class'],\n    'pre': ['class'],\n    'table': ['class']\n  },\n  allowedSchemes: ['https']\n};\n\n\/\/ Nessun HTML: rimuove tutto\nconst stripAll = (dirty) => sanitizeHtml(dirty, {\n  allowedTags: [],\n  allowedAttributes: {}\n});\n\nmodule.exports = {\n  sanitizeComment: (html) => sanitizeHtml(html, commentOptions),\n  sanitizeArticle: (html) => sanitizeHtml(html, articleOptions),\n  stripAll\n};<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Salva sempre l'HTML sanitizzato nel database, non l'HTML grezzo. Sanitizzare solo al momento della visualizzazione lascia dati pericolosi persistenti: se la logica di display cambia o viene introdotto un bug che bypassa il sanitizzatore, il payload XSS salvato nel database diventa attivo. La sanitizzazione pre-persist \u00e8 l'unica strategia difendibile a lungo termine.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-10-validazione-file-upload-sicura\">Step 10: Validazione file upload sicura<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">L'upload di file \u00e8 una delle superfici di attacco pi\u00f9 pericolose nelle API REST. Un attaccante pu\u00f2 caricare file eseguibili rinominati con estensione immagine, file con nomi che traversano directory come <code>..\/..\/etc\/passwd<\/code>, o file enormi che esauriscono lo storage. La validazione deve operare su tre livelli: estensione dichiarata, tipo MIME dichiarato dal browser e magic bytes reali del file.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ middleware\/validateUpload.js\nconst multer = require('multer');\nconst path = require('path');\nconst crypto = require('crypto');\nconst { fileTypeFromBuffer } = require('file-type');\n\nconst ALLOWED_MIME_TYPES = new Set(['image\/jpeg', 'image\/png', 'image\/webp', 'image\/gif']);\nconst MIME_TO_EXT = { 'image\/jpeg': '.jpg', 'image\/png': '.png', 'image\/webp': '.webp', 'image\/gif': '.gif' };\n\nconst storage = multer.diskStorage({\n  destination: '\/tmp\/uploads',\n  filename: (req, file, cb) => {\n    \/\/ Nome file casuale: previene path traversal e caratteri speciali\n    const randomName = crypto.randomBytes(16).toString('hex');\n    const ext = MIME_TO_EXT[file.mimetype] || '.bin';\n    cb(null, `${randomName}${ext}`);\n  }\n});\n\nconst fileFilter = (req, file, cb) => {\n  if (!ALLOWED_MIME_TYPES.has(file.mimetype)) {\n    return cb(new Error(`Tipo file non supportato: ${file.mimetype}`), false);\n  }\n  const ext = path.extname(file.originalname).toLowerCase();\n  if (!['.jpg', '.jpeg', '.png', '.webp', '.gif'].includes(ext)) {\n    return cb(new Error('Estensione file non supportata'), false);\n  }\n  cb(null, true);\n};\n\nconst upload = multer({\n  storage,\n  fileFilter,\n  limits: {\n    fileSize: 5 * 1024 * 1024,  \/\/ 5 MB\n    files: 1,\n    fields: 5\n  }\n});\n\n\/\/ Verifica il tipo reale tramite magic bytes (non il MIME dichiarato)\nconst verifyFileContent = async (req, res, next) => {\n  if (!req.file) return next();\n  const fs = require('fs').promises;\n  try {\n    const buffer = await fs.readFile(req.file.path);\n    const detected = await fileTypeFromBuffer(buffer);\n    if (!detected || !ALLOWED_MIME_TYPES.has(detected.mime)) {\n      await fs.unlink(req.file.path);\n      return res.status(422).json({\n        success: false,\n        message: 'Il contenuto del file non corrisponde al tipo dichiarato'\n      });\n    }\n    req.file.verifiedMime = detected.mime;\n    next();\n  } catch (err) {\n    await fs.unlink(req.file.path).catch(() => {});\n    next(err);\n  }\n};\n\nmodule.exports = { upload, verifyFileContent };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-11-validazione-asincrona-con-accesso-al-database\">Step 11: Validazione asincrona con accesso al database<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Alcune regole di validazione richiedono l'accesso al database: verificare che un username non sia gi\u00e0 registrato, che un codice coupon esista e non sia scaduto, che un prodotto appartenga all'utente autenticato prima di permettere la modifica. Tutte e tre le librerie supportano validatori asincroni, con sintassi leggermente diverse.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ Validazione asincrona con express-validator\nconst { body } = require('express-validator');\nconst { getUserByEmail } = require('..\/db\/queries');\n\nconst uniqueEmailRule = body('email')\n  .trim()\n  .normalizeEmail()\n  .isEmail()\n  .withMessage('Email non valida')\n  .custom(async (email) => {\n    const existing = await getUserByEmail(email);\n    if (existing) {\n      throw new Error('Email gi\u00e0 registrata. Usa \"Accedi\" oppure \"Password dimenticata\".');\n    }\n    \/\/ Non \u00e8 necessario return true; se non si lancia un'eccezione, la validazione passa\n  });\n\n\/\/ Validazione asincrona con Joi (validateAsync obbligatorio)\nconst Joi = require('joi');\n\nconst registrationSchema = Joi.object({\n  username: Joi.string().trim().min(3).max(30)\n    .external(async (value) => {\n      const taken = await checkUsernameExists(value);\n      if (taken) throw new Error('Username non disponibile: scegli un altro');\n    })\n    .required(),\n  email: Joi.string().email().lowercase().required()\n});\n\nconst validateRegistration = async (req, res, next) => {\n  try {\n    \/\/ IMPORTANTE: per schemi con validatori asincroni usa validateAsync, non validate\n    const value = await registrationSchema.validateAsync(req.body, {\n      abortEarly: false,\n      stripUnknown: true\n    });\n    req.body = value;\n    next();\n  } catch (error) {\n    if (error.isJoi) {\n      return res.status(422).json({\n        success: false,\n        errors: error.details.map(d => ({ field: d.path[0], message: d.message }))\n      });\n    }\n    next(error); \/\/ Errori di database passati al gestore errori globale\n  }\n};<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-12-testing-della-validazione-con-jest-e-supertest\">Step 12: Testing della validazione con Jest e Supertest<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I test di validazione devono coprire sia il percorso felice (input validi) sia ogni caso di errore: campi mancanti obbligatori, formati errati, valori fuori range, payload SQL injection, payload XSS e tentativi di mass-assignment. Un test per ogni regola di validazione \u00e8 il livello minimo accettabile per la produzione.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ tests\/validation.test.js\nconst request = require('supertest');\nconst app = require('..\/app');\n\ndescribe('Validazione input API utenti', () => {\n\n  test('Registrazione con dati validi: risposta 201', async () => {\n    const res = await request(app)\n      .post('\/api\/users\/register')\n      .send({ username: 'mario_rossi', email: 'mario@example.com', password: 'Secure!Pass2026' });\n    expect(res.status).toBe(201);\n    expect(res.body.success).toBe(true);\n  });\n\n  test('SQL injection nel campo username: risposta 422', async () => {\n    const res = await request(app)\n      .post('\/api\/users\/register')\n      .send({\n        username: \"'; DROP TABLE users; --\",\n        email: 'test@example.com',\n        password: 'Secure!Pass2026'\n      });\n    expect(res.status).toBe(422);\n    expect(res.body.errors.some(e => e.field === 'username')).toBe(true);\n  });\n\n  test('XSS nel campo email: risposta 422', async () => {\n    const res = await request(app)\n      .post('\/api\/users\/register')\n      .send({\n        username: 'mariorossi',\n        email: '<script>alert(1)<\/script>@evil.com',\n        password: 'Secure!Pass2026'\n      });\n    expect(res.status).toBe(422);\n  });\n\n  test('Mass assignment con isAdmin=true: campo rimosso dalla risposta', async () => {\n    const res = await request(app)\n      .post('\/api\/users\/register')\n      .send({\n        username: 'giulia_bianchi',\n        email: 'giulia@example.com',\n        password: 'Secure!Pass2026',\n        isAdmin: true,\n        role: 'superadmin'\n      });\n    \/\/ I campi extra devono essere rimossi dallo schema\n    expect(res.body.data?.isAdmin).toBeUndefined();\n    expect(res.body.data?.role).toBeUndefined();\n  });\n\n  test('Password senza simboli: risposta 422 con campo password', async () => {\n    const res = await request(app)\n      .post('\/api\/users\/register')\n      .send({ username: 'luca_verdi', email: 'luca@example.com', password: 'semplicissima123' });\n    expect(res.status).toBe(422);\n    expect(res.body.errors.some(e => e.field === 'password')).toBe(true);\n  });\n\n  test('Payload oltre 10 KB: risposta 413', async () => {\n    const res = await request(app)\n      .post('\/api\/users\/register')\n      .send({ username: 'a'.repeat(11000) });\n    expect(res.status).toBe(413);\n  });\n\n  test('Raccolta errori multipli (abortEarly: false)', async () => {\n    const res = await request(app)\n      .post('\/api\/users\/register')\n      .send({ username: 'x', email: 'non-una-email', password: 'corta' });\n    expect(res.status).toBe(422);\n    \/\/ Almeno 3 errori: username troppo corto, email invalida, password troppo corta\n    expect(res.body.errors.length).toBeGreaterThanOrEqual(3);\n  });\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"7-errori-comuni-nella-validazione-degli-input\">7 errori comuni nella validazione degli input<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>1. Validare solo lato client.<\/strong> JavaScript nel browser pu\u00f2 essere disabilitato o aggirato con curl, Postman o Burp Suite. La validazione lato client \u00e8 una comodit\u00e0 UX, non una misura di sicurezza. Ogni dato deve essere rivalidato sul server prima di essere elaborato, indipendentemente dalla validazione front-end gi\u00e0 implementata.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>2. Non usare <code>abortEarly: false<\/code> in Joi.<\/strong> Con la configurazione di default, Joi restituisce solo il primo errore trovato. L'utente deve correggere il form un campo alla volta per scoprire tutti gli errori. Imposta sempre <code>abortEarly: false<\/code> nei middleware di validazione per raccogliere e restituire tutti gli errori in una sola risposta.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>3. Omettere <code>stripUnknown: true<\/code> in Joi o non usare <code>.strip()<\/code> in Zod.<\/strong> Senza questa opzione, i campi extra inviati dall'utente (come <code>isAdmin<\/code>, <code>role<\/code>, <code>balance<\/code>) passano la validazione e raggiungono il database. In Zod il comportamento di default \u00e8 gi\u00e0 <code>strip<\/code>, ma in Joi devi attivarlo esplicitamente.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>4. Usare <code>.escape()<\/code> su HTML ricco.<\/strong> <code>body('bio').escape()<\/code> converte <code>&lt;b&gt;testo&lt;\/b&gt;<\/code> in <code>&amp;lt;b&amp;gt;<\/code>, rompendo qualsiasi formattazione HTML. Per testo che deve contenere HTML sicuro (bio utente, descrizioni prodotto), usa <code>sanitize-html<\/code> con una whitelist di tag permessi.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>5. Validare i file solo per estensione o MIME dichiarato.<\/strong> Il browser invia il tipo MIME basandosi sull'estensione del file dichiarata dal sistema operativo. Rinominare <code>malware.php<\/code> in <code>foto.jpg<\/code> inganna la validazione basata solo su MIME. La verifica tramite magic bytes con <code>file-type<\/code> analizza i byte iniziali del file e rileva il tipo reale indipendentemente dall'estensione.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>6. Non validare i parametri URL.<\/strong> <code>req.params.id<\/code> \u00e8 sempre una stringa. Una route <code>GET \/users\/:id<\/code> con <code>id = \"..\/admin\"<\/code> o <code>id = \"0 UNION SELECT * FROM users--\"<\/code> pu\u00f2 causare comportamenti inattesi. Valida sempre i parametri URL con <code>param()<\/code> in express-validator o con <code>validateParams()<\/code> e uno schema Zod\/Joi.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>7. Usare validatori asincroni senza <code>validateAsync<\/code> in Joi.<\/strong> Se uno schema Joi include <code>.external()<\/code> o <code>Joi.when()<\/code> con funzioni async, chiamare <code>schema.validate()<\/code> invece di <code>schema.validateAsync()<\/code> ignora silenziosamente le promesse dei validatori asincroni. Tutti i validatori async vengono saltati senza errori, rendendo la validazione parziale e potenzialmente insicura.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"troubleshooting-8-problemi-frequenti\">Troubleshooting: 8 problemi frequenti<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Il body della richiesta \u00e8 undefined.<\/strong> Express non analizza il body JSON automaticamente. Verifica che <code>app.use(express.json())<\/code> sia dichiarato prima di tutte le route nel file principale. In Express 5.2.1 questo middleware \u00e8 ancora necessario esplicitamente.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>validationResult restituisce sempre un array vuoto.<\/strong> Le regole di express-validator devono essere eseguite come middleware prima del controller. Verifica che la route includa le regole come array: <code>router.post('\/path', [body('field').isEmail()], handleValidation, controller)<\/code>. Se il middleware manca o \u00e8 nell'ordine sbagliato, la validazione non viene mai eseguita.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Joi non valida oggetti annidati.<\/strong> La validazione di oggetti annidati richiede <code>Joi.object()<\/code> ricorsivi. Definire solo <code>Joi.string()<\/code> su un campo che riceve un oggetto genera un errore di tipo generico, non di validazione del contenuto interno. Usa la struttura: <code>address: Joi.object({ street: Joi.string().required() })<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Zod 4 rompe il codice scritto per Zod 3.<\/strong> Zod 4 ha introdotto breaking changes rispetto a Zod 3. La sintassi per i messaggi di errore \u00e8 cambiata in alcune funzioni. Prima di aggiornare un progetto da Zod 3 a Zod 4, consulta il changelog ufficiale e verifica i test. Il pacchetto <code>zod-v3<\/code> esiste per chi deve mantenere la compatibilit\u00e0 con Zod 3.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>sanitize-html rimuove tutto l'HTML.<\/strong> Le versioni recenti di sanitize-html hanno una whitelist di default vuota. Se chiami <code>sanitizeHtml(html)<\/code> senza opzioni, tutto l'HTML viene rimosso. Specifica sempre le opzioni: usa <code>allowedTags: sanitizeHtml.defaults.allowedTags<\/code> per la lista di default o definisci la tua whitelist.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Multer non rileva il tipo MIME corretto.<\/strong> <code>file.mimetype<\/code> in Multer rispecchia il <code>Content-Type<\/code> inviato dal browser, non il contenuto reale del file. Un file PHP rinominato in <code>.jpg<\/code> passa la validazione MIME. Usa <code>fileTypeFromBuffer()<\/code> dopo l'upload temporaneo per verificare il tipo reale tramite i magic bytes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>I test superano ma gli attacchi funzionano in produzione.<\/strong> I test con dati hardcoded testano solo i casi che il programmatore ha immaginato. Aggiungi test con payload di injection reali (vedi Step 12), valori limite (stringa vuota, null, array invece di stringa, numero molto grande) e payload di fuzzing. Strumenti come Burp Suite o OWASP ZAP automatizzano questo tipo di testing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>CORS blocca le richieste prima della validazione.<\/strong> Il middleware CORS deve essere dichiarato prima delle route e dei middleware di validazione in Express. Se il preflight CORS restituisce 403, la validazione non viene mai raggiunta e gli errori di validazione non vengono mai restituiti al client. Usa <code>app.use(cors(options))<\/code> come primo middleware.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"suggerimenti-avanzati\">Suggerimenti avanzati<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"valida-le-variabili-dambiente-allavvio\">Valida le variabili d'ambiente all'avvio<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Usa Zod per validare tutte le variabili d'ambiente all'avvio dell'applicazione. Un errore di configurazione (porta mancante, stringa di connessione malformata, secret JWT troppo corto) viene rilevato immediatamente prima che l'app accetti traffico, invece di causare errori runtime in produzione a ore o giorni dall'avvio.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code\">\/\/ config\/env.js\nconst { z } = require('zod');\n\nconst EnvSchema = z.object({\n  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),\n  PORT: z.coerce.number().int().positive().default(3000),\n  DATABASE_URL: z.string().url({ message: 'DATABASE_URL deve essere un URL valido' }),\n  JWT_SECRET: z.string().min(32, 'JWT_SECRET: minimo 32 caratteri'),\n  SESSION_SECRET: z.string().min(32, 'SESSION_SECRET: minimo 32 caratteri'),\n  ALLOWED_ORIGINS: z.string()\n    .transform(s => s.split(',').map(o => o.trim()))\n    .default('http:\/\/localhost:3000')\n});\n\nconst result = EnvSchema.safeParse(process.env);\n\nif (!result.success) {\n  console.error('[CONFIG] Variabili d\\'ambiente non valide:');\n  result.error.issues.forEach(i =>\n    console.error(`  ${i.path.join('.')}: ${i.message}`)\n  );\n  process.exit(1);\n}\n\nmodule.exports = result.data;<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"integra-validazione-e-rate-limiting\">Integra validazione e rate limiting<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">La validazione e il rate limiting sono difese complementari. Gli endpoint che eseguono query al database per validazione asincrona (verifica unicit\u00e0 email, disponibilit\u00e0 username) sono particolarmente vulnerabili: ogni richiesta genera un accesso al database, e un attaccante pu\u00f2 inviare migliaia di richieste per esaurire le connessioni disponibili. Applica rate limiting pi\u00f9 restrittivo sugli endpoint con validazione asincrona: 5 richieste per 10 minuti per la registrazione, invece delle 100 al minuto delle API generali.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Consulta la guida <a href=\"\/it\/rate-limiting-nodejs\/\">Rate Limiting in Node.js<\/a> per configurare <code>express-rate-limit<\/code> con Redis per ambienti multi-istanza. La combinazione di validazione degli input, rate limiting e HMAC per le API interne copre la maggior parte dei vettori di attacco per un'API REST Node.js in produzione.<\/p>\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=\"guide-di-sicurezza-node-js\">Guide di sicurezza Node.js<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/it\/owasp-top-10-nodejs-2026\/\">OWASP Top 10 2025 in Node.js: 10 Vulnerabilit\u00e0 e 12 Difese<\/a> - il contesto di sicurezza completo che include la validazione degli input come difesa contro A03:2021<\/li>\n<li><a href=\"\/it\/autenticazione-jwt-nodejs\/\">Autenticazione JWT in Node.js: 12 Step<\/a> - come validare e verificare i token JWT nelle API REST protette<\/li>\n<li><a href=\"\/it\/protezione-csrf-nodejs\/\">Protezione CSRF in Node.js: 12 Step<\/a> - difesa dai Cross-Site Request Forgery, complementare alla validazione del body<\/li>\n<li><a href=\"\/it\/rate-limiting-nodejs\/\">Rate Limiting in Node.js: API Sicura in 12 Step<\/a> - limita gli abusi degli endpoint di registrazione e autenticazione<\/li>\n<li><a href=\"\/it\/oauth2-openid-connect-nodejs\/\">OAuth 2.0 e OpenID Connect in Node.js: 12 Step<\/a> - autenticazione delegata con validazione dei token access e ID token<\/li>\n<li><a href=\"\/it\/burp-suite-tutorial\/\">Burp Suite: Test di Sicurezza Web in 12 Step<\/a> - come testare automaticamente la resistenza della tua API agli attacchi di injection<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"domande-frequenti\">Domande frequenti<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"devo-usare-zod-o-joi-nel-2026\">Devo usare Zod o Joi nel 2026?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Usa Zod se il tuo progetto usa TypeScript: l'inferenza automatica dei tipi con <code>z.infer<\/code> elimina la duplicazione tra schemi di validazione e interfacce TypeScript. Usa Joi se lavori con JavaScript puro e hai schemi molto complessi con dipendenze condizionali multiple (<code>Joi.when()<\/code> annidati). Con 201 milioni di download settimanali contro i 23 milioni di Joi, Zod ha gi\u00e0 superato Joi in popolarit\u00e0 nel 2025-2026, trainato dall'adozione di TypeScript nel backend Node.js.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"la-validazione-degli-input-protegge-dalla-sql-injection\">La validazione degli input protegge dalla SQL injection?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">La validazione riduce la superficie di attacco rifiutando pattern ovviamente malevoli. Ma non sostituisce le query parametrizzate: anche un input perfettamente valido come l'apostrofo nel cognome O'Brien causa SQL injection in una query concatenata. Usa sempre entrambe le difese insieme: validazione per il formato, query parametrizzate per la sicurezza del database.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"dove-eseguo-la-validazione-nel-middleware-o-nel-controller\">Dove eseguo la validazione: nel middleware o nel controller?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Nel middleware, prima del controller. Il controller deve ricevere dati gi\u00e0 validati e sanitizzati e concentrarsi solo sulla logica di business. La validazione nel controller mescola responsabilit\u00e0 diverse, rende pi\u00f9 difficile il riutilizzo degli schemi e complica il testing separato della logica di validazione.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"express-validator-o-joi-per-aggiornare-un-progetto-express-esistente\">express-validator o Joi per aggiornare un progetto Express esistente?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">express-validator per una migrazione graduale: puoi aggiungere validazione a una route alla volta senza ristrutturare il codice esistente. Ogni route diventa indipendente. Joi o Zod per nuovi moduli o per una refactoring completa: la separazione schema\/middleware porta a un codice pi\u00f9 leggibile e manutenibile nel lungo periodo.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"come-gestisco-la-validazione-con-microservizi-multipli\">Come gestisco la validazione con microservizi multipli?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Pubblica gli schemi di validazione come pacchetto npm interno condiviso tra i servizi. Con Zod, lo stesso pacchetto esporta sia lo schema sia il tipo TypeScript inferito. Ogni servizio importa lo schema e lo usa per validare i propri input, garantendo consistenza delle regole di validazione senza duplicazione. Il pacchetto interno diventa la fonte di verit\u00e0 per il contratto API tra i servizi.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"come-valido-un-array-di-oggetti-con-express-validator\">Come valido un array di oggetti con express-validator?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Usa la notazione con wildcard <code>*<\/code>: <code>body('items.*.productId').isUUID()<\/code> valida il campo <code>productId<\/code> di ogni elemento dell'array <code>items<\/code>. Combina con <code>body('items').isArray({ min: 1, max: 50 })<\/code> per validare anche la dimensione dell'array. Per strutture annidate complesse con molti livelli, Joi e Zod offrono un'API pi\u00f9 leggibile e meno soggetta a errori di sintassi.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"quanto-rallenta-lapi-la-validazione-con-zod-o-joi\">Quanto rallenta l'API la validazione con Zod o Joi?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">L'overhead per richieste normali \u00e8 trascurabile. Zod 4 ha introdotto ottimizzazioni significative rispetto a Zod 3, con validazioni sincrone tipiche che completano in meno di 1 ms. L'overhead diventa misurabile solo con array di migliaia di elementi o con validatori asincroni che accedono al database in cascata. In questi casi, separa la validazione sincrona del formato da quella asincrona di unicit\u00e0, ed applica la seconda solo dopo che la prima ha avuto successo.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>La validazione degli input in Node.js non \u00e8 un dettaglio di implementazione: \u00e8 la prima linea di difesa contro SQL injection, XSS, command injection e una dozzina di altri attacchi\u2026<\/p>\n","protected":false},"author":4,"featured_media":217,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-216","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\/216","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=216"}],"version-history":[{"count":0,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/posts\/216\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/media\/217"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/media?parent=216"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/categories?post=216"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/tags?post=216"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}