{"id":218,"date":"2026-06-18T17:10:50","date_gmt":"2026-06-18T17:10:50","guid":{"rendered":"https:\/\/shattered.io\/it\/2026\/06\/18\/sql-injection-nodejs\/"},"modified":"2026-06-18T17:10:50","modified_gmt":"2026-06-18T17:10:50","slug":"sql-injection-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/it\/2026\/06\/18\/sql-injection-nodejs\/","title":{"rendered":"SQL Injection in Node.js: Come Prevenirla in 12 Step [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Un singolo apice mal gestito pu\u00f2 esporre l&#8217;intero database di produzione. SQL injection rimane nel 2026 la seconda vulnerabilit\u00e0 per numero di CVE: CWE-89 ha prodotto 3.349 CVE nel solo 2025, con una crescita del 75% rispetto all&#8217;anno precedente. Compare come A05 nell&#8217;OWASP Top 10 2025, e secondo SecureLayer7 colpisce il 100% delle applicazioni web testate. Questa guida mostra come costruire un&#8217;API Node.js resistente a ogni variante di SQL injection, dal classico attacco UNION alle tecniche blind e time-based, usando query parametrizzate, ORM sicuri e difese in profondit\u00e0.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"cose-sql-injection-e-perche-conta-ancora-nel-2026\">Cos&#8217;\u00e8 SQL Injection e Perch\u00e9 Conta Ancora nel 2026<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">SQL injection (SQLi) si verifica quando dati non validati forniti dall&#8217;utente vengono concatenati direttamente in una query SQL, permettendo all&#8217;attaccante di modificare la logica della query stessa. L&#8217;esempio pi\u00f9 noto \u00e8 il login bypass: se l&#8217;applicazione costruisce la query come <code>SELECT * FROM users WHERE username='${input}'<\/code>, passare <code>' OR '1'='1<\/code> come username produce una condizione sempre vera, bypassando l&#8217;autenticazione senza conoscere alcuna password.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Secondo il Verizon Data Breach Investigations Report 2026, l&#8217;exploitation di vulnerabilit\u00e0 ha causato il 31% di tutte le violazioni, rispetto al 20% del 2025. CWE-89 (SQL Injection) \u00e8 la categoria di vulnerabilit\u00e0 con il maggior volume di CVE nel 2025. Il costo medio globale di una violazione dati \u00e8 salito a 4,44 milioni di dollari nel 2025 (IBM Cost of a Data Breach Report 2025), mentre il mercato statunitense tocca i 10,22 milioni di dollari, un record storico. Il tempo medio di remediation per le vulnerabilit\u00e0 di injection supera i 13 mesi per raggiungere il 50% di risoluzione, il dato peggiore tra tutte le categorie OWASP secondo il DBIR 2026.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le varianti di SQL injection rilevanti per Node.js sono cinque. La <strong>classica (in-band)<\/strong>, dove il risultato appare direttamente nella risposta HTTP. La <strong>UNION-based<\/strong>, che sfrutta l&#8217;operatore UNION per estrarre dati da altre tabelle. La <strong>blind boolean-based<\/strong>, dove l&#8217;attaccante deduce informazioni dalla differenza nel comportamento dell&#8217;app (risposta vera vs. falsa). La <strong>time-based<\/strong>, che usa funzioni come <code>SLEEP()<\/code> o <code>pg_sleep()<\/code> per inferire dati dai ritardi di risposta. E l&#8217;<strong>out-of-band<\/strong>, che sfrutta DNS o HTTP per esfiltrare dati verso un server controllato dall&#8217;attaccante.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Variante SQLi<\/th><th>Tecnica<\/th><th>Rilevabilit\u00e0<\/th><th>Danno tipico<\/th><\/tr><\/thead><tbody><tr><td>In-band classica<\/td><td>Apice + iniezione diretta<\/td><td>Alta (errori visibili)<\/td><td>Dump completo del DB<\/td><\/tr><tr><td>UNION-based<\/td><td>UNION SELECT su colonne aggiuntive<\/td><td>Media<\/td><td>Lettura di qualsiasi tabella<\/td><\/tr><tr><td>Blind boolean<\/td><td>Condizioni vero\/falso<\/td><td>Bassa<\/td><td>Estrazione lenta dei dati<\/td><\/tr><tr><td>Time-based blind<\/td><td>SLEEP() \/ pg_sleep()<\/td><td>Molto bassa<\/td><td>Estrazione via timing<\/td><\/tr><tr><td>Out-of-band<\/td><td>DNS\/HTTP exfiltration<\/td><td>Quasi nulla<\/td><td>Esfiltrazione silenziosa<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"prerequisiti\">Prerequisiti<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Per seguire questa guida servono:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Node.js<\/strong> 22.x LTS o superiore (versione consigliata per le patch CVE-2026-21710 e CVE-2026-21715 che toccano il runtime)<\/li><li><strong>npm<\/strong> 10.x o superiore<\/li><li><strong>PostgreSQL<\/strong> 16+ oppure <strong>MySQL<\/strong> 8.4+ oppure <strong>SQLite<\/strong> 3.45+ (per i test locali)<\/li><li>Conoscenza base di Express.js e async\/await in Node.js<\/li><li>Accesso a un terminale Unix o PowerShell su Windows<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Le librerie usate in questa guida e le loro versioni correnti a giugno 2026:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Libreria<\/th><th>Versione<\/th><th>Database supportati<\/th><th>Tipo<\/th><\/tr><\/thead><tbody><tr><td>pg (node-postgres)<\/td><td>8.x<\/td><td>PostgreSQL<\/td><td>Driver nativo<\/td><\/tr><tr><td>mysql2<\/td><td>3.x<\/td><td>MySQL, MariaDB<\/td><td>Driver nativo<\/td><\/tr><tr><td>Prisma<\/td><td>6.x<\/td><td>Postgres, MySQL, SQLite, MSSQL<\/td><td>ORM type-safe<\/td><\/tr><tr><td>Sequelize<\/td><td>6.x<\/td><td>Postgres, MySQL, SQLite, MSSQL<\/td><td>ORM classico<\/td><\/tr><tr><td>Knex.js<\/td><td>3.x<\/td><td>Postgres, MySQL, SQLite, MSSQL<\/td><td>Query builder<\/td><\/tr><tr><td>zod<\/td><td>3.x<\/td><td>n\/a<\/td><td>Validazione schema<\/td><\/tr><tr><td>helmet<\/td><td>8.x<\/td><td>n\/a<\/td><td>Header HTTP sicuri<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-1-inizializzare-il-progetto-node-js-sicuro\">Step 1: Inizializzare il Progetto Node.js Sicuro<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Crea la struttura base del progetto. Useremo PostgreSQL come database principale, ma i principi si applicano identici a MySQL e SQLite. L&#8217;intera struttura serve come punto di partenza per una API Express production-ready, con middleware di sicurezza attivi fin dall&#8217;inizio.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir sqli-safe-api && cd sqli-safe-api\nnpm init -y\n\n# Driver database (installa solo quello che usi)\nnpm install pg           # PostgreSQL\nnpm install mysql2       # MySQL \/ MariaDB\n\n# ORM e query builder (scegli uno)\nnpm install @prisma\/client prisma   # Prisma ORM\nnpm install sequelize               # Sequelize ORM\nnpm install knex                    # Knex.js query builder\n\n# Framework, validazione e sicurezza\nnpm install express zod helmet express-rate-limit\n\n# Dev tools\nnpm install -D nodemon<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Crea il file <code>app.js<\/code> con la struttura Express di base e i middleware di sicurezza attivati dall&#8217;inizio:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app.js\nconst express   = require('express');\nconst helmet    = require('helmet');\nconst rateLimit = require('express-rate-limit');\n\nconst app = express();\n\n\/\/ Helmet imposta automaticamente 11 header HTTP di sicurezza:\n\/\/ CSP, HSTS, X-Frame-Options, X-Content-Type-Options, ecc.\napp.use(helmet());\n\n\/\/ Limita il body JSON a 10 KB: previene attacchi di body stuffing\napp.use(express.json({ limit: '10kb' }));\n\n\/\/ Rate limiting: max 100 richieste ogni 15 minuti per IP\nconst apiLimiter = rateLimit({\n  windowMs:      15 * 60 * 1000,\n  max:           100,\n  standardHeaders: true,\n  legacyHeaders:   false,\n  message:       { error: 'Troppe richieste, riprova tra 15 minuti' },\n});\napp.use('\/api\/', apiLimiter);\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => console.log(`Server avviato sulla porta ${PORT}`));\n\nmodule.exports = app;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-2-il-codice-vulnerabile-da-non-usare-mai\">Step 2: Il Codice Vulnerabile (da Non Usare Mai)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Prima di mostrare la soluzione corretta, \u00e8 necessario capire il pattern vulnerabile che appare ancora in migliaia di repository Node.js su GitHub. Questo codice \u00e8 intenzionalmente insicuro: non usarlo mai in produzione, nemmeno come punto di partenza da &#8220;aggiustare dopo&#8221;.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ CODICE VULNERABILE - NON USARE IN PRODUZIONE\nconst { Pool } = require('pg');\nconst pool = new Pool({ connectionString: process.env.DATABASE_URL });\n\napp.get('\/api\/users\/search', async (req, res) => {\n  const { username } = req.query;\n\n  \/\/ ERRORE CRITICO: concatenazione diretta dell'input nella query SQL\n  const query = `SELECT id, email, role FROM users WHERE username = '${username}'`;\n\n  try {\n    const result = await pool.query(query);\n    res.json(result.rows);\n  } catch (err) {\n    \/\/ SECONDO ERRORE: espone stack trace e testo della query all'attaccante\n    res.status(500).json({ error: err.message, query: query });\n  }\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Con questo codice, un attaccante pu\u00f2 passare <code>' OR '1'='1' --<\/code> come username e ottenere tutti gli utenti del database. Con <code>' UNION SELECT table_name, null, null FROM information_schema.tables --<\/code> pu\u00f2 elencare tutte le tabelle. La variante time-based <code>' OR pg_sleep(5) --<\/code> conferma la vulnerabilit\u00e0 tramite il ritardo di risposta senza esporre alcun errore visibile.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Attenzione anche all&#8217;anti-pattern del filtraggio manuale con blacklist di caratteri. Rimuovere semplicemente gli apici non \u00e8 sufficiente: esistono bypass tramite encoding esadecimale (<code>0x61646d696e<\/code>), bypass con doppio encoding, e tecniche che non usano affatto gli apici (injection su colonne numeriche come <code>1 OR 1=1<\/code>). L&#8217;unica difesa affidabile \u00e8 la query parametrizzata.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-3-query-parametrizzate-con-pg-postgresql\">Step 3: Query Parametrizzate con pg (PostgreSQL)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Il metodo corretto usa le <strong>query parametrizzate<\/strong> (dette anche prepared statements): i valori forniti dall&#8217;utente vengono inviati al database come parametri tipizzati, separati dalla struttura della query SQL. Il database non li interpreta mai come codice SQL, indipendentemente dal contenuto.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ drivers\/postgres.js\nconst { Pool } = require('pg');\n\nconst pool = new Pool({\n  connectionString: process.env.DATABASE_URL,\n  ssl: process.env.NODE_ENV === 'production'\n    ? { rejectUnauthorized: true }\n    : false,\n  max:                    10,\n  idleTimeoutMillis:      30000,\n  connectionTimeoutMillis: 5000,\n  statement_timeout:      30000,  \/\/ Blocca query che durano pi\u00f9 di 30s\n  query_timeout:          30000,\n});\n\nmodule.exports = pool;<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ routes\/users.js\nconst pool = require('..\/drivers\/postgres');\n\n\/\/ SICURO: query parametrizzata - $1 \u00e8 un placeholder tipizzato\napp.get('\/api\/users\/search', async (req, res, next) => {\n  try {\n    const { username } = req.query;\n\n    \/\/ Il valore di username viene inviato al DB su un canale separato\n    \/\/ Anche se contiene ' OR '1'='1, il DB la tratta come stringa letterale\n    const result = await pool.query(\n      'SELECT id, email, role FROM users WHERE username = $1',\n      [username]\n    );\n\n    res.json(result.rows);\n  } catch (err) {\n    next(err);  \/\/ Passa all'error handler centrale, non espone err.message\n  }\n});\n\n\/\/ Query con pi\u00f9 parametri: $1, $2, $3... in ordine\napp.post('\/api\/users', async (req, res, next) => {\n  try {\n    const { username, email, role } = req.body;\n\n    const result = await pool.query(\n      `INSERT INTO users (username, email, role, created_at)\n       VALUES ($1, $2, $3, NOW())\n       RETURNING id, username, email, role`,\n      [username, email, role]\n    );\n\n    res.status(201).json(result.rows[0]);\n  } catch (err) {\n    next(err);\n  }\n});\n\n\/\/ Prepared statement esplicito: il piano di esecuzione viene calcolato una volta sola\n\/\/ e riusato per tutte le chiamate successive (vantaggio prestazionale + sicurezza)\nasync function getUserById(id) {\n  return pool.query({\n    name:   'get-user-by-id',\n    text:   'SELECT id, username, email, role FROM users WHERE id = $1',\n    values: [parseInt(id, 10)],\n  });\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Con pg, ogni query parametrizzata invia al server PostgreSQL un messaggio &#8220;Parse&#8221; con la query template e un messaggio &#8220;Bind&#8221; con i valori. Il driver garantisce che i valori non vengano mai interpolati nella stringa SQL. Per query eseguite frequentemente, i named prepared statement offrono anche un vantaggio prestazionale: il piano di esecuzione viene calcolato una sola volta e riusato per tutte le chiamate successive della stessa connessione.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-4-query-parametrizzate-con-mysql2-mysql-e-mariadb\">Step 4: Query Parametrizzate con mysql2 (MySQL e MariaDB)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Per MySQL e MariaDB, <code>mysql2<\/code> usa il placeholder <code>?<\/code> invece di <code>$1<\/code>. C&#8217;\u00e8 per\u00f2 una differenza critica rispetto a pg: con mysql2 esistono due metodi con comportamenti di sicurezza diversi.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ drivers\/mysql.js\nconst mysql = require('mysql2\/promise');\n\nconst pool = mysql.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\n  \/\/ IMPORTANTE: disabilita le query multi-statement\n  \/\/ Previene attacchi tipo '; DROP TABLE users; --'\n  multipleStatements: false,\n\n  \/\/ SSL in produzione\n  ssl: process.env.NODE_ENV === 'production'\n    ? { rejectUnauthorized: true }\n    : undefined,\n\n  connectionLimit: 10,\n  waitForConnections: true,\n  queueLimit: 0,\n});\n\nmodule.exports = pool;<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ CRITICO: usa .execute() non .query() per le prepared statement\n\/\/ .execute() -> prepared statement veri (sicuri)\n\/\/ .query()   -> client-side interpolation (meno sicuro)\n\n\/\/ SICURO: execute() con prepared statement\napp.get('\/api\/products', async (req, res, next) => {\n  try {\n    const { category, maxPrice } = req.query;\n\n    \/\/ I ? vengono sostituiti in ordine dall'array di valori\n    const [rows] = await pool.execute(\n      'SELECT id, name, price, stock FROM products WHERE category = ? AND price <= ? AND active = TRUE',\n      [category, Number(maxPrice)]\n    );\n\n    res.json(rows);\n  } catch (err) {\n    next(err);\n  }\n});\n\n\/\/ Query con IN(): mysql2 espande gli array in placeholder multipli\napp.get('\/api\/products\/batch', async (req, res, next) => {\n  try {\n    const ids = req.query.ids?.split(',').map(Number).filter(n => !isNaN(n)) || [];\n    if (!ids.length) return res.status(400).json({ error: 'IDs richiesti' });\n\n    \/\/ Genera i placeholder dinamicamente in modo sicuro\n    const placeholders = ids.map(() => '?').join(', ');\n    const [rows] = await pool.query(\n      `SELECT id, name, price FROM products WHERE id IN (${placeholders})`,\n      ids\n    );\n\n    res.json(rows);\n  } catch (err) {\n    next(err);\n  }\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-5-sql-injection-prevention-con-prisma-orm\">Step 5: SQL Injection Prevention con Prisma ORM<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Prisma \u00e8 il pi\u00f9 popolare ORM type-safe per Node.js nel 2026. Tutte le query generate da Prisma Client attraverso le sue API standard usano automaticamente query parametrizzate: non c&#8217;\u00e8 modo di iniettare SQL tramite i metodi dell&#8217;ORM. Il rischio rimane solo con le raw query, che richiedono attenzione esplicita.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ prisma\/schema.prisma (schema di esempio)\n\/\/\n\/\/ generator client {\n\/\/   provider = \"prisma-client-js\"\n\/\/ }\n\/\/ datasource db {\n\/\/   provider = \"postgresql\"\n\/\/   url      = env(\"DATABASE_URL\")\n\/\/ }\n\/\/ model User {\n\/\/   id       Int    @id @default(autoincrement())\n\/\/   username String @unique @db.VarChar(50)\n\/\/   email    String @unique @db.VarChar(255)\n\/\/   role     String @default(\"user\") @db.VarChar(20)\n\/\/ }\n\nconst { PrismaClient } = require('@prisma\/client');\nconst prisma = new PrismaClient({\n  log: ['warn', 'error'],  \/\/ Non loggare le query in produzione (privacy)\n});\n\n\/\/ SICURO: metodi ORM di Prisma - sempre parametrizzati automaticamente\napp.get('\/api\/users\/search', async (req, res, next) => {\n  try {\n    const { username, role, page = 1, limit = 20 } = req.query;\n    const offset = (Number(page) - 1) * Number(limit);\n\n    const users = await prisma.user.findMany({\n      where: {\n        AND: [\n          username ? { username: { contains: username, mode: 'insensitive' } } : {},\n          role     ? { role: role }                                            : {},\n        ],\n      },\n      select: { id: true, username: true, email: true, role: true },\n      take:   Math.min(Number(limit), 50),  \/\/ Limite massimo: 50\n      skip:   offset,\n      orderBy: { id: 'asc' },\n    });\n\n    res.json({ data: users, page: Number(page) });\n  } catch (err) {\n    next(err);\n  }\n});\n\n\/\/ RAW QUERY con Prisma: usa SEMPRE i tagged template literals (sicuri)\n\/\/ Prisma estrae automaticamente le variabili come parametri separati\napp.get('\/api\/users\/:id\/stats', async (req, res, next) => {\n  try {\n    const userId = parseInt(req.params.id, 10);\n    if (isNaN(userId) || userId < 1) {\n      return res.status(400).json({ error: 'ID utente non valido' });\n    }\n\n    \/\/ SICURO: template literal - ${userId} diventa un parametro, non sintassi SQL\n    const stats = await prisma.$queryRaw`\n      SELECT u.id, u.username, COUNT(o.id)::int AS order_count,\n             SUM(o.total)::numeric AS total_spent\n      FROM users u\n      LEFT JOIN orders o ON o.user_id = u.id\n      WHERE u.id = ${userId}\n      GROUP BY u.id, u.username\n    `;\n\n    res.json(stats[0] ?? null);\n  } catch (err) {\n    next(err);\n  }\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Il punto critico con Prisma \u00e8 la distinzione tra le raw query sicure e quelle pericolose. <code>prisma.$queryRaw<\/code> con tagged template literal \u00e8 sicuro perch\u00e9 Prisma estrae i valori interpolati e li invia come parametri. Al contrario, <code>prisma.$queryRawUnsafe(string)<\/code> \u00e8 pericoloso quanto la concatenazione manuale e va usato solo con stringhe completamente statiche, mai con input utente.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-6-query-sicure-con-sequelize-e-knex-js\">Step 6: Query Sicure con Sequelize e Knex.js<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Sequelize e Knex.js offrono API diverse ma lo stesso livello di protezione quando usati correttamente. Entrambi parametrizzano automaticamente i valori passati attraverso le loro API standard, con un'area di rischio specifica nelle raw query.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ === SEQUELIZE ===\nconst { Sequelize, DataTypes, Op } = require('sequelize');\nconst sequelize = new Sequelize(process.env.DATABASE_URL, {\n  logging: false,\n  dialectOptions: {\n    ssl: process.env.NODE_ENV === 'production'\n      ? { require: true, rejectUnauthorized: true }\n      : false,\n  },\n});\n\n\/\/ Metodi ORM di Sequelize: parametrizzati automaticamente\napp.get('\/api\/users', async (req, res, next) => {\n  try {\n    const { search, role } = req.query;\n    const users = await User.findAll({\n      where: {\n        ...(search ? { username: { [Op.iLike]: `%${search}%` } } : {}),\n        ...(role   ? { role: role }                              : {}),\n      },\n      attributes: ['id', 'username', 'email', 'role'],\n      limit: 20,\n    });\n    res.json(users);\n  } catch (err) { next(err); }\n});\n\n\/\/ Raw query con Sequelize: usa SEMPRE replacements, non interpolazione\napp.get('\/api\/reports\/by-role', async (req, res, next) => {\n  try {\n    const { role } = req.query;\n    const [results] = await sequelize.query(\n      'SELECT role, COUNT(*) AS total FROM users WHERE role = :role GROUP BY role',\n      {\n        replacements: { role },              \/\/ Sicuro: parametro named\n        type: Sequelize.QueryTypes.SELECT,\n      }\n    );\n    res.json(results);\n  } catch (err) { next(err); }\n});\n\n\/\/ === KNEX.JS ===\nconst knex = require('knex')({\n  client:     'pg',\n  connection: process.env.DATABASE_URL,\n  pool:       { min: 2, max: 10 },\n});\n\n\/\/ Query builder di Knex: parametrizzato automaticamente\napp.get('\/api\/products', async (req, res, next) => {\n  try {\n    const { category, minPrice, maxPrice, sort = 'price', order = 'asc' } = req.query;\n\n    \/\/ Whitelist per ordinamento dinamico (nomi colonne non parametrizzabili)\n    const ALLOWED_COLS   = new Set(['id', 'name', 'price', 'created_at']);\n    const ALLOWED_ORDERS = new Set(['asc', 'desc']);\n    const safeSort  = ALLOWED_COLS.has(sort)    ? sort    : 'price';\n    const safeOrder = ALLOWED_ORDERS.has(order) ? order   : 'asc';\n\n    const products = await knex('products')\n      .select('id', 'name', 'price', 'stock', 'category')\n      .where('category', category)\n      .whereBetween('price', [Number(minPrice) || 0, Number(maxPrice) || 999999])\n      .where('active', true)\n      .orderBy(safeSort, safeOrder)\n      .limit(100);\n\n    res.json(products);\n  } catch (err) { next(err); }\n});\n\n\/\/ Raw con Knex: usa knex.raw() con binding espliciti\napp.get('\/api\/products\/featured', async (req, res, next) => {\n  try {\n    const { category } = req.query;\n    const result = await knex.raw(\n      'SELECT id, name, price FROM products WHERE category = ? AND featured = TRUE LIMIT 10',\n      [category]  \/\/ Array di binding: sicuro\n    );\n    res.json(result.rows);\n  } catch (err) { next(err); }\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-7-validazione-dellinput-con-zod\">Step 7: Validazione dell'Input con Zod<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le query parametrizzate proteggono dalla SQL injection. La validazione dell'input \u00e8 una difesa in profondit\u00e0 che blocca l'attacco ancora prima che raggiunga il database. Con Zod puoi definire schemi rigorosi per ogni endpoint e rifiutare input malformati con messaggi di errore chiari, senza esporre dettagli interni.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const { z } = require('zod');\n\n\/\/ Schema per ricerca utenti\nconst UserSearchSchema = z.object({\n  q:     z.string().max(100)\n           .regex(\/^[\\w\\s@._-]+$\/, 'Caratteri non consentiti')\n           .optional(),\n  role:  z.enum(['admin', 'user', 'moderator']).optional(),\n  page:  z.coerce.number().int().min(1).max(100).default(1),\n  limit: z.coerce.number().int().min(1).max(50).default(20),\n});\n\n\/\/ Schema per creazione utente\nconst CreateUserSchema = z.object({\n  username: z.string().min(3).max(50)\n              .regex(\/^[a-zA-Z0-9_]+$\/, 'Solo lettere, numeri e underscore'),\n  email:    z.string().email().max(255).transform(v => v.toLowerCase()),\n  password: z.string().min(12).max(128),\n  role:     z.enum(['user', 'moderator']).default('user'),\n});\n\n\/\/ Middleware di validazione riusabile\nfunction validate(schema, source = 'query') {\n  return (req, res, next) => {\n    const data = source === 'body' ? req.body : req.query;\n    const result = schema.safeParse(data);\n\n    if (!result.success) {\n      return res.status(400).json({\n        error:  'Dati non validi',\n        issues: result.error.flatten().fieldErrors,\n      });\n    }\n\n    req.validated = result.data;\n    next();\n  };\n}\n\n\/\/ Applicazione: validazione + query parametrizzata\napp.get('\/api\/users',\n  validate(UserSearchSchema, 'query'),\n  async (req, res, next) => {\n    try {\n      const { q, role, page, limit } = req.validated;\n      const offset = (page - 1) * limit;\n\n      \/\/ Query con LIKE sicuro: il wildcard % \u00e8 nel valore (non nella query)\n      const result = await pool.query(\n        `SELECT id, username, email, role\n         FROM users\n         WHERE ($1::text IS NULL OR username ILIKE '%' || $1 || '%')\n           AND ($2::text IS NULL OR role = $2)\n         ORDER BY id ASC\n         LIMIT $3 OFFSET $4`,\n        [q ?? null, role ?? null, limit, offset]\n      );\n\n      res.json({ data: result.rows, page, limit });\n    } catch (err) {\n      next(err);\n    }\n  }\n);<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-8-gestione-sicura-di-nomi-di-colonne-e-tabelle-dinamici\">Step 8: Gestione Sicura di Nomi di Colonne e Tabelle Dinamici<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le query parametrizzate proteggono i <em>valori<\/em>, non i nomi di colonne o tabelle. Se l'applicazione costruisce query con nomi di colonne dinamici (ad esempio per permettere all'utente di scegliere il campo di ordinamento), devi usare una whitelist esplicita. Questo \u00e8 uno dei punti dove molti sviluppatori cadono in un falso senso di sicurezza.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ PATTERN PERICOLOSO: ordinamento dinamico senza whitelist\n\/\/ VULNERABILE a: GET \/api\/users?sort=1;SELECT+pg_sleep(5)--\n\/\/ NON FARE: `ORDER BY ${req.query.sort}` nemmeno con pg\n\n\/\/ PATTERN SICURO: whitelist esplicita di colonne e ordini consentiti\nconst ALLOWED_SORT_COLUMNS = new Set([\n  'id', 'username', 'email', 'created_at', 'updated_at', 'role'\n]);\nconst ALLOWED_SORT_ORDERS = new Set(['ASC', 'DESC']);\n\nfunction parseSafeOrderBy(column, order) {\n  const safeCol   = ALLOWED_SORT_COLUMNS.has(column)\n    ? column\n    : 'id';\n  const safeOrder = ALLOWED_SORT_ORDERS.has(order?.toUpperCase())\n    ? order.toUpperCase()\n    : 'ASC';\n  return { safeCol, safeOrder };\n}\n\napp.get('\/api\/users\/sorted', async (req, res, next) => {\n  try {\n    const { sort = 'id', order = 'ASC', search } = req.query;\n    const { safeCol, safeOrder } = parseSafeOrderBy(sort, order);\n\n    \/\/ I nomi colonna provengono dalla whitelist (costanti, non input utente)\n    \/\/ Il valore di search \u00e8 un parametro ($1)\n    const query = `\n      SELECT id, username, email, role, created_at\n      FROM users\n      WHERE ($1::text IS NULL OR username ILIKE '%' || $1 || '%')\n      ORDER BY ${safeCol} ${safeOrder}\n      LIMIT 50\n    `;\n\n    const result = await pool.query(query, [search ?? null]);\n    res.json(result.rows);\n  } catch (err) {\n    next(err);\n  }\n});\n\n\/\/ Per nomi di tabelle dinamici: stessa logica con whitelist\nconst ALLOWED_TABLES = new Set(['users', 'products', 'orders', 'categories']);\n\nfunction safeTable(name) {\n  if (!ALLOWED_TABLES.has(name)) {\n    throw Object.assign(new Error('Tabella non consentita'), { statusCode: 400 });\n  }\n  return name; \/\/ Safe: proveniente dalla whitelist\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-9-configurazione-sicura-del-database\">Step 9: Configurazione Sicura del Database<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La difesa in profondit\u00e0 richiede anche una configurazione del database che limiti il danno in caso di compromissione parziale. Il principio del privilegio minimo (least privilege) riduce drasticamente l'impatto di un attacco riuscito: anche se un attaccante ottiene l'accesso tramite una vulnerabilit\u00e0 non ancora patchata, i danni che pu\u00f2 fare sono circoscritti ai soli privilegi dell'utente applicativo.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>-- PostgreSQL: crea un utente applicazione con soli privilegi necessari\n-- Esegui come superuser (DBA)\n\nCREATE ROLE app_user WITH LOGIN PASSWORD 'usa_un_secret_manager_non_questa_stringa';\n\nGRANT CONNECT ON DATABASE myapp_db TO app_user;\nGRANT USAGE ON SCHEMA public TO app_user;\n\n-- Solo le operazioni necessarie per le tabelle applicative\nGRANT SELECT, INSERT, UPDATE, DELETE\n  ON TABLE users, products, orders, sessions, refresh_tokens\n  TO app_user;\n\n-- Sequence per gli auto-increment\nGRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user;\n\n-- Nega DROP, TRUNCATE, ALTER (non assegnati = negati per default)\n-- Nega l'accesso alle tabelle di sistema\nREVOKE ALL ON ALL TABLES IN SCHEMA information_schema FROM PUBLIC;\nREVOKE ALL ON ALL TABLES IN SCHEMA pg_catalog FROM PUBLIC;\n\n-- Timeout per prevenire attacchi time-based con sleep lunghe\nALTER ROLE app_user SET statement_timeout = '30s';\nALTER ROLE app_user SET lock_timeout = '10s';\nALTER ROLE app_user SET idle_in_transaction_session_timeout = '60s';<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Per le credenziali del database, non inserire mai stringhe di connessione hardcoded nel codice sorgente. In sviluppo usa un file <code>.env<\/code> escluso da git. In produzione usa AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, o le variabili d'ambiente dell'orchestratore (Kubernetes Secrets, Docker Secrets). Un segreto leakato nel repository Git pu\u00f2 essere recuperato dalla history anche dopo la cancellazione, e va considerato compromesso permanentemente.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-10-logging-e-rilevamento-degli-attacchi\">Step 10: Logging e Rilevamento degli Attacchi<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">SQL injection lascia tracce caratteristiche nei log: errori di sintassi SQL, query con tempi di risposta anomali, e pattern di input sospetti. Configura il logging strutturato con Pino per rilevare questi segnali in tempo reale e alimentare il tuo SIEM.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const pino = require('pino');\n\nconst logger = pino({\n  level: process.env.LOG_LEVEL ?? 'info',\n  \/\/ Non loggare mai credenziali o dati sensibili\n  redact: [\n    'req.headers.authorization',\n    'req.body.password',\n    'req.body.token',\n    '*.secret',\n  ],\n});\n\n\/\/ Pattern caratteristici di SQL injection nell'input\nconst SQLI_PATTERNS = [\n  \/(\\b(UNION|SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|EXEC|EXECUTE)\\b)\/i,\n  \/(--|\\\/\\*|\\*\\\/|;--)\/,\n  \/'\\s*(OR|AND)\\s+['\"]?\\d+['\"]?\\s*[=<>]\/i,\n  \/pg_sleep|sleep\\s*\\(|benchmark\\s*\\(\/i,\n  \/information_schema|sys\\.tables|sysobjects|sqlite_master\/i,\n  \/\\bload_file\\s*\\(|outfile\\b\/i,  \/\/ MySQL file read\/write\n];\n\nfunction hasSQLiSignature(value) {\n  return typeof value === 'string' && SQLI_PATTERNS.some(p => p.test(value));\n}\n\n\/\/ Middleware di rilevamento: blocca prima del controller\nfunction sqliDetectionMiddleware(req, res, next) {\n  const inputs = [\n    ...Object.values(req.query ?? {}),\n    ...Object.values(req.body ?? {}),\n    ...Object.values(req.params ?? {}),\n  ].filter(v => typeof v === 'string');\n\n  if (inputs.some(hasSQLiSignature)) {\n    logger.warn({\n      event:    'sqli_attempt',\n      ip:       req.ip,\n      method:   req.method,\n      path:     req.path,\n      ua:       req.get('User-Agent'),\n      \/\/ Logga solo i nomi dei campi sospetti, non i valori (evita log injection)\n      fields:   [\n        ...Object.keys(req.query ?? {}),\n        ...Object.keys(req.body ?? {}),\n      ],\n    });\n\n    return res.status(400).json({ error: 'Richiesta non valida' });\n  }\n\n  next();\n}\n\napp.use('\/api\/', sqliDetectionMiddleware);\n\n\/\/ Middleware per query DB lente (potenziali attacchi time-based)\npool.on('connect', client => {\n  const originalQuery = client.query.bind(client);\n  client.query = function (...args) {\n    const start = Date.now();\n    const promise = originalQuery.apply(this, args);\n    Promise.resolve(promise).then(() => {\n      const ms = Date.now() - start;\n      if (ms > 2000) {\n        logger.warn({ event: 'slow_db_query', duration_ms: ms });\n      }\n    }).catch(() => {});\n    return promise;\n  };\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-11-testing-automatico-per-sql-injection\">Step 11: Testing Automatico per SQL Injection<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ogni endpoint che riceve input utente deve essere testato per SQL injection. Integra i test automatici nella pipeline CI\/CD con Jest e Supertest per bloccare le regressioni prima che raggiungano la produzione.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ tests\/sqli.test.js\nconst request = require('supertest');\nconst app     = require('..\/app');\n\n\/\/ Payload di test standard da OWASP Testing Guide\nconst SQLI_PAYLOADS = [\n  \"' OR '1'='1\",\n  \"' OR '1'='1' --\",\n  \"' OR '1'='1' \/*\",\n  \"' UNION SELECT null, username, password FROM users --\",\n  \"'; DROP TABLE users; --\",\n  \"1 OR 1=1\",\n  \"1; SELECT * FROM users\",\n  \"' OR pg_sleep(2) --\",             \/\/ Time-based (PostgreSQL)\n  \"admin'--\",\n  \"%27 OR %271%27=%271\",             \/\/ URL-encoded\n  \"' OR 1=1#\",                       \/\/ MySQL comment\n  \"1 OR 1=1--\",                      \/\/ Senza apice (colonne numeriche)\n];\n\ndescribe('SQL Injection Prevention', () => {\n  describe('GET \/api\/users - ricerca username', () => {\n    SQLI_PAYLOADS.forEach((payload) => {\n      test(`rifiuta o gestisce correttamente payload: ${payload.slice(0, 25)}...`, async () => {\n        const res = await request(app)\n          .get('\/api\/users')\n          .query({ q: payload });\n\n        \/\/ Non deve mai restituire un errore 500 con dettagli SQL\n        expect(res.status).not.toBe(500);\n\n        const body = JSON.stringify(res.body);\n\n        \/\/ Non deve esporre messaggi di errore interni del DB\n        expect(body).not.toMatch(\/syntax error\/i);\n        expect(body).not.toMatch(\/postgresql\/i);\n        expect(body).not.toMatch(\/sql state\/i);\n        expect(body).not.toMatch(\/column .+ does not exist\/i);\n\n        \/\/ Se risponde 200, non deve restituire pi\u00f9 utenti del previsto\n        if (res.status === 200 && Array.isArray(res.body.data)) {\n          \/\/ Un payload SQLi che bypassa WHERE restituirebbe tutti gli utenti\n          \/\/ Un'app con 1000+ utenti non dovrebbe restituire 50+ risultati per un payload\n          expect(res.body.data.length).toBeLessThan(50);\n        }\n      }, 5000); \/\/ Timeout 5s per rilevare attacchi time-based\n    });\n  });\n\n  test('POST \/api\/users rifiuta email con caratteri SQL', async () => {\n    const res = await request(app)\n      .post('\/api\/users')\n      .send({\n        username: 'testuser',\n        email:    \"test@example.com'; DROP TABLE users; --\",\n        password: 'ValidPassword123!',\n      });\n\n    expect([400, 422]).toContain(res.status);\n  });\n\n  test('GET \/api\/users\/:id con ID non numerico ritorna 400', async () => {\n    const res = await request(app)\n      .get(\"\/api\/users\/' OR 1=1 --\");\n\n    expect([400, 404]).toContain(res.status);\n  });\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Affianca i test unitari con OWASP ZAP in modalit\u00e0 automatica. Aggiungi questo step alla pipeline CI\/CD di GitHub Actions o GitLab CI per eseguire uno scan dell'ambiente di staging prima di ogni deploy in produzione. ZAP rileva varianti di SQL injection che i test unitari possono perdere, incluse le tecniche blind e time-based.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-12-difese-in-profondita-e-progetto-completo\">Step 12: Difese in Profondit\u00e0 e Progetto Completo<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le query parametrizzate sono la difesa primaria. Le misure seguenti creano strati di protezione indipendenti che limitano il danno anche se un singolo livello fallisce per un errore umano o una configurazione errata.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Web Application Firewall (WAF):<\/strong> Cloudflare WAF, AWS WAF con le managed rules OWASP Core Rule Set (CRS), o ModSecurity (open source) analizzano il traffico HTTP prima che raggiunga Node.js e bloccano i pattern SQLi pi\u00f9 comuni. Attiva le OWASP CRS managed rules sul tuo WAF e configura il modo \"block\" invece di \"log only\".<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Row-Level Security (RLS) in PostgreSQL:<\/strong> Abilita RLS sulle tabelle con dati multi-tenant. Con RLS, l'utente <code>app_user<\/code> vede solo le righe del tenant corrente, limitando il danno di una injection al singolo tenant anche se l'attaccante riesce a bypassare la logica applicativa.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>-- PostgreSQL RLS: isolamento per tenant\nALTER TABLE orders ENABLE ROW LEVEL SECURITY;\n\nCREATE POLICY tenant_isolation ON orders\n  FOR ALL\n  TO app_user\n  USING (tenant_id = current_setting('app.tenant_id', true)::int);\n\n-- In Node.js: imposta il contesto del tenant all'inizio di ogni richiesta\nasync function withTenantContext(pool, tenantId, fn) {\n  const client = await pool.connect();\n  try {\n    await client.query('BEGIN');\n    \/\/ Il cast a intero previene injection anche qui\n    await client.query(\n      `SET LOCAL app.tenant_id = ${parseInt(tenantId, 10)}`\n    );\n    const result = await fn(client);\n    await client.query('COMMIT');\n    return result;\n  } catch (err) {\n    await client.query('ROLLBACK');\n    throw err;\n  } finally {\n    client.release();\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Il progetto completo che integra tutti i pattern di questa guida \u00e8 disponibile su GitHub. La struttura finale del progetto include:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sqli-safe-api\/\n\u251c\u2500\u2500 app.js              # Entry point, middleware di sicurezza\n\u251c\u2500\u2500 drivers\/\n\u2502   \u251c\u2500\u2500 postgres.js     # Pool PostgreSQL con timeout e SSL\n\u2502   \u2514\u2500\u2500 mysql.js        # Pool MySQL con multipleStatements: false\n\u251c\u2500\u2500 middleware\/\n\u2502   \u251c\u2500\u2500 validate.js     # Middleware Zod generico\n\u2502   \u251c\u2500\u2500 sqliDetect.js   # Rilevamento pattern SQLi\n\u2502   \u2514\u2500\u2500 errorHandler.js # Error handler centrale (niente stack trace)\n\u251c\u2500\u2500 routes\/\n\u2502   \u251c\u2500\u2500 users.js        # Endpoint utenti con query parametrizzate\n\u2502   \u2514\u2500\u2500 products.js     # Endpoint prodotti con Knex.js\n\u251c\u2500\u2500 schemas\/\n\u2502   \u251c\u2500\u2500 user.js         # Schema Zod per User\n\u2502   \u2514\u2500\u2500 product.js      # Schema Zod per Product\n\u251c\u2500\u2500 tests\/\n\u2502   \u2514\u2500\u2500 sqli.test.js    # Test automatici SQLi con payload OWASP\n\u251c\u2500\u2500 prisma\/\n\u2502   \u2514\u2500\u2500 schema.prisma   # Schema Prisma ORM\n\u251c\u2500\u2500 .env.example        # Template variabili d'ambiente (no credenziali)\n\u2514\u2500\u2500 package.json<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"errori-comuni-pitfall-da-evitare\">Errori Comuni (Pitfall da Evitare)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Questi sono gli errori pi\u00f9 frequenti che vanificano la protezione da SQL injection in applicazioni Node.js reali, ordinati per frequenza riscontrata nei code review di sicurezza.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pitfall 1: Usare <code>.query()<\/code> invece di <code>.execute()<\/code> con mysql2.<\/strong> Con mysql2, <code>connection.query()<\/code> usa l'interpolazione lato client (escape di stringhe), non prepared statement veri a livello di protocollo MySQL. <code>connection.execute()<\/code> invia la query e i parametri al server in messaggi separati. Per input utente, usa sempre <code>execute()<\/code>. L'eccezione \u00e8 la query con array IN(), dove serve costruire i placeholder dinamicamente.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pitfall 2: <code>prisma.$queryRawUnsafe()<\/code> con input utente.<\/strong> Questo metodo \u00e8 equivalente alla concatenazione manuale. Esiste per casi in cui la query deve essere costruita programmaticamente con valori gi\u00e0 validati. Usalo solo con stringhe completamente statiche o con valori passati come argomento separato alla funzione, mai con template string che interpolano input utente.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pitfall 3: Filtrare solo gli apici singoli.<\/strong> Rimuovere o fare escape degli apici non \u00e8 sufficiente. Molte varianti di SQL injection non usano apici: <code>1 OR 1=1<\/code> su campi numerici, injection via commenti <code>\/**\/<\/code>, doppio encoding Unicode. L'unica difesa affidabile \u00e8 la parametrizzazione.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pitfall 4: Esporre messaggi di errore SQL al client.<\/strong> Un messaggio come <code>ERROR: syntax error at or near \"'\"<\/code> o <code>Table 'users' doesn't exist<\/code> conferma la vulnerabilit\u00e0 e rivela informazioni sulla struttura del database. Usa un error handler centrale che mappi tutti gli errori DB a <code>{ error: 'Errore interno del server' }<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pitfall 5: Non validare il tipo dei parametri numerici.<\/strong> Se un endpoint accetta un ID numerico via URL (es. <code>\/api\/users\/:id<\/code>) e non valida che sia effettivamente un intero, un input come <code>1 OR 1=1<\/code> pu\u00f2 raggiungere la query. Converti e valida sempre: <code>const id = parseInt(req.params.id, 10); if (isNaN(id)) return 400;<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pitfall 6: Ordinamento dinamico senza whitelist.<\/strong> Costruire <code>ORDER BY ${req.query.sort}<\/code> \u00e8 vulnerabile anche se i valori della clausola WHERE sono parametrizzati. I nomi di identificatori SQL non possono essere parametrizzati: usa sempre una whitelist con un <code>Set<\/code> di colonne consentite.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pitfall 7: Second-order injection.<\/strong> L'input viene memorizzato nel DB (correttamente, con parametrizzazione) e poi riusato in una query successiva senza parametrizzazione. Esempio: un username memorizzato come <code>admin'--<\/code> che viene poi usato per costruire una query di report. Parametrizza ogni query che usa dati provenienti dal DB, anche se quei dati erano stati inseriti in modo sicuro.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"risoluzione-dei-problemi-comuni\">Risoluzione dei Problemi Comuni<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Problema<\/th><th>Causa probabile<\/th><th>Soluzione<\/th><\/tr><\/thead><tbody><tr><td>Query parametrizzata ritorna 0 risultati dove ci si aspettano dati<\/td><td>Tipo del parametro non corrisponde alla colonna (stringa vs intero)<\/td><td>Cast esplicito: <code>$1::int<\/code> oppure <code>parseInt(value, 10)<\/code> prima del parametro<\/td><\/tr><tr><td>Errore \"invalid input syntax for type integer\"<\/td><td>Input non numerico su una colonna intera<\/td><td>Valida prima con Zod: <code>z.coerce.number().int()<\/code>; ritorna 400 se fallisce<\/td><\/tr><tr><td>ILIKE con parametro non funziona con pg<\/td><td>Il wildcard % deve stare nel valore, non nella query template<\/td><td>Usa <code>values: ['%' + search + '%']<\/code> oppure <code>ILIKE '%' || $1 || '%'<\/code> nella query<\/td><\/tr><tr><td>Errore \"prepared statement X already exists\" con pg<\/td><td>Stesso nome statement usato per query diverse sulla stessa connessione<\/td><td>Usa nomi univoci per statement, oppure evita i named statement con pool<\/td><\/tr><tr><td>mysql2 execute() lancia errore su IN(?)<\/td><td>mysql2 non espande automaticamente gli array per execute()<\/td><td>Genera i placeholder: <code>ids.map(() => '?').join(',')<\/code> e usa pool.query()<\/td><\/tr><tr><td>Prisma $queryRaw lancia errore di tipo<\/td><td>Uso di template literal non-tagged invece di tagged template<\/td><td>Sintassi corretta: <code>prisma.$queryRaw\\`SELECT ... WHERE id = ${id}\\`<\/code><\/td><\/tr><tr><td>ZAP segnala SQLi nonostante le parametrizzazioni<\/td><td>ZAP usa fuzzing: rileva differenze di comportamento tra input valido e invalido<\/td><td>Normalizza i messaggi di errore: stessa struttura JSON per 400, 404, 500<\/td><\/tr><tr><td>Test SQLi passano in locale ma falliscono in staging<\/td><td>Versione diversa del driver o configurazione del pool diversa<\/td><td>Pinna le versioni esatte in package-lock.json; usa lo stesso .env in entrambi gli ambienti<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"consigli-avanzati-per-la-produzione\">Consigli Avanzati per la Produzione<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Analisi statica (SAST) con Semgrep:<\/strong> Integra Semgrep nella pipeline CI\/CD con le regole del registry <code>r2c-security-audit<\/code>. Semgrep rileva automaticamente i pattern di concatenazione di input in query SQL nel codice Node.js e pu\u00f2 essere configurato con regole personalizzate per le specifiche del progetto. Si integra con GitHub Actions in 5 righe di YAML e blocca il PR se trova vulnerabilit\u00e0.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Database Activity Monitoring (DAM):<\/strong> Abilita <code>pgaudit<\/code> su PostgreSQL per registrare ogni query eseguita con l'utente che l'ha lanciata, il timestamp, e se ha avuto successo. In caso di incidente, hai un audit trail completo. Configura alert via il tuo SIEM (Elastic, Splunk) per pattern anomali: numero di righe estratte superiore alla media, query UNION non attese, esecuzione di funzioni come <code>pg_sleep<\/code> o <code>load_file<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rotazione automatica delle credenziali:<\/strong> In ambienti cloud, configura la rotazione automatica delle password del database tramite AWS Secrets Manager Rotation Lambda, GCP Secret Manager, o Vault Dynamic Secrets. Il pool di connessioni deve ricaricare le credenziali senza restart dell'applicazione: usa l'hook <code>connect<\/code> del pool per aggiornare le credenziali alla creazione di ogni nuova connessione.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Analisi delle dipendenze con npm audit e Socket.dev:<\/strong> Esegui <code>npm audit --audit-level=high<\/code> in CI e blocca il deploy se rileva vulnerabilit\u00e0 HIGH o CRITICAL nelle dipendenze. Integra Socket.dev per il monitoraggio in tempo reale delle supply chain attack sulle dipendenze npm, incluse le librerie di accesso al DB.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"copertura-correlata\">Copertura Correlata<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Approfondisci la sicurezza delle applicazioni Node.js con queste guide pratiche su shattered.io.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"\/it\/owasp-top-10-nodejs-2026\/\">OWASP Top 10 2025 in Node.js: 10 Vulnerabilit\u00e0, 12 Difese<\/a> - la panoramica completa delle vulnerabilit\u00e0 pi\u00f9 critiche e come mitigarle<\/li><li><a href=\"\/it\/validazione-input-nodejs\/\">Validazione Input in Node.js con Zod, Joi e express-validator in 12 Step<\/a> - validazione approfondita per tutti i tipi di input con schemi rigorosi<\/li><li><a href=\"\/it\/protezione-csrf-nodejs\/\">Protezione CSRF in Node.js<\/a> - difendersi dagli attacchi Cross-Site Request Forgery con token e SameSite cookies<\/li><li><a href=\"\/it\/rate-limiting-nodejs\/\">Rate Limiting in Node.js: API Sicura in 12 Step<\/a> - limitare i tentativi di brute force e abuse con express-rate-limit e Redis<\/li><li><a href=\"\/it\/autenticazione-jwt-nodejs\/\">Autenticazione JWT in Node.js: 12 Step<\/a> - gestione sicura dei JSON Web Token con rotazione e revoca<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"risorse-esterne\">Risorse Esterne<\/h2>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/SQL_Injection_Prevention_Cheat_Sheet.html\" rel=\"noopener noreferrer\" target=\"_blank\">OWASP SQL Injection Prevention Cheat Sheet<\/a> - riferimento definitivo per le contromisure, con esempi per ogni framework<\/li><li><a href=\"https:\/\/portswigger.net\/web-security\/sql-injection\" rel=\"noopener noreferrer\" target=\"_blank\">PortSwigger Web Security Academy: SQL Injection<\/a> - laboratori pratici e spiegazioni approfondite sulle tecniche di attacco<\/li><li><a href=\"https:\/\/owasp.org\/www-project-top-ten\/\" rel=\"noopener noreferrer\" target=\"_blank\">OWASP Top 10 Project<\/a> - lista ufficiale delle 10 vulnerabilit\u00e0 pi\u00f9 critiche per le applicazioni web<\/li><li><a href=\"https:\/\/sequelize.org\/docs\/v6\/core-concepts\/raw-queries\/\" rel=\"noopener noreferrer\" target=\"_blank\">Sequelize Raw Queries Documentation<\/a> - guida ufficiale alle query raw sicure con replacements e bind<\/li><li><a href=\"https:\/\/www.zaproxy.org\/\" rel=\"noopener noreferrer\" target=\"_blank\">OWASP ZAP (Zed Attack Proxy)<\/a> - scanner di vulnerabilit\u00e0 open source per test automatici SQLi<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"faq\">FAQ<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Le query parametrizzate proteggono da tutte le varianti di SQL injection?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le query parametrizzate proteggono da tutte le varianti di injection nei <em>valori<\/em>: classica, UNION-based, blind boolean e time-based. Non proteggono dai nomi di colonne o tabelle dinamici, per i quali serve la whitelist. Non proteggono nemmeno dagli attacchi second-order injection, dove l'input viene memorizzato nel DB e usato successivamente in una query non parametrizzata. Parametrizza ogni query che riceve dati potenzialmente controllati dall'utente, anche indirettamente.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Un ORM come Prisma o Sequelize elimina completamente il rischio?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Gli ORM riducono significativamente il rischio perch\u00e9 le query generate dalle API standard sono sempre parametrizzate. Il rischio rimane con le raw query (<code>$queryRawUnsafe<\/code>, <code>sequelize.query()<\/code> senza replacements, <code>knex.raw()<\/code> senza binding). Tratta ogni raw query con lo stesso rigore di una query scritta a mano.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Qual \u00e8 la differenza tra escape e parametrizzazione?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">L'escape converte i caratteri pericolosi prima di inserirli nella stringa SQL. La parametrizzazione non inserisce mai il valore nella stringa SQL: lo invia al database su un canale separato come valore tipizzato. La parametrizzazione \u00e8 pi\u00f9 sicura perch\u00e9 non dipende dalla correttezza dell'implementazione dell'escape e funziona correttamente anche con encoding inusuali e set di caratteri multibyte.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Come proteggo i nomi di tabelle dinamici?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I nomi di tabelle e colonne non possono essere parametrizzati in SQL. L'unica soluzione sicura \u00e8 una whitelist: un <code>Set<\/code> JavaScript con i nomi consentiti. Prima di usare un nome di tabella dinamico, verifica la sua presenza nella whitelist e lancia un errore 400 se non \u00e8 consentito. Non usare mai l'input utente come nome di tabella o colonna senza whitelist, nemmeno dopo l'escape.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Devo usare una WAF se gi\u00e0 uso query parametrizzate?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">S\u00ec, come difesa in profondit\u00e0. Le query parametrizzate sono la difesa primaria e pi\u00f9 efficace. Una WAF aggiunge un layer di protezione che blocca attacchi prima che raggiungano l'applicazione, riduce la superficie di attacco per gli errori umani, e rileva tentativi in tempo reale per la risposta agli incidenti. I due strumenti si complementano.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Cosa fare se scopro una SQL injection in produzione?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Passi immediati: 1) Isola l'endpoint vulnerabile con un blocco WAF o disabilitandolo temporaneamente. 2) Analizza i log degli ultimi 90 giorni per verificare se l'attacco \u00e8 gi\u00e0 avvenuto. 3) Controlla l'integrit\u00e0 dei dati sensibili. 4) Applica la patch con query parametrizzate in un branch di emergenza e deploya. 5) Se hai evidenza di exfiltrazione di dati personali, attiva il processo di notifica GDPR: 72 ore per comunicarlo all'autorit\u00e0 di controllo (il Garante Privacy in Italia). 6) Documenta l'incidente nel registro dei trattamenti.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Come verifico se la mia applicazione \u00e8 vulnerabile?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Usa OWASP ZAP per uno scan automatico in staging. Integra Semgrep con le regole <code>r2c-security-audit<\/code> per l'analisi statica del codice. Esegui i test manuali con i payload descritti nello Step 11. Per un'analisi professionale, affida un penetration test a un esperto certificato (OSCP, CEH, GPEN) almeno una volta all'anno.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Le stored procedure sono pi\u00f9 sicure delle query parametrizzate?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le stored procedure offrono un livello di sicurezza simile alle query parametrizzate quando i parametri vengono passati correttamente. Il vantaggio aggiuntivo \u00e8 che incapsulano la logica SQL nel database, riducendo il codice esposto nell'applicazione. Lo svantaggio \u00e8 la maggiore complessit\u00e0 di manutenzione e il lock-in verso un database specifico. Per la maggior parte delle applicazioni Node.js, le query parametrizzate con un ORM offrono il miglior equilibrio tra sicurezza, portabilit\u00e0 e manutenibilit\u00e0.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Un singolo apice mal gestito pu\u00f2 esporre l&#8217;intero database di produzione. SQL injection rimane nel 2026 la seconda vulnerabilit\u00e0 per numero di CVE: CWE-89 ha prodotto 3.349 CVE nel solo\u2026<\/p>\n","protected":false},"author":6,"featured_media":219,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-218","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\/218","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\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/comments?post=218"}],"version-history":[{"count":0,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/posts\/218\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/media\/219"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/media?parent=218"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/categories?post=218"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/tags?post=218"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}