SQL (Structured query language) o volgarmente chiamato sequel ma la qui pronuncia ufficiale (per non assomigliare a quella del vecchio SEQUEL) è /ɛskjuːˈɛl/ è un linguaggio standardizzato utilizzato in informatica per interrogare i DBMS relazionali.
Aspetta… due nuove parole! speghiamoci meglio…
I DBMS (Database Managment System) sono quelli che volgarmente chiamiamo “Database” ovverò software utilizzati per memorizzare i dati in modo ordinato e/o strutturato.
Per database relazionali sono database composti di tabelle collegate tra di loro da relazioni di accoppiamento. Un esempio che chiarisca potrebbe essere una tabella contenente nome, cognome e città di residenza e una tabella in cui sono inserite le città, la regione di appartenenza e lo stato. In questo caso sembra chiaro che la città associata ad ogni nome è legata da una relazione N:1 con le città contenute nell’altra tabella. Il che significa che ogni riga di persone contiene solo una delle città mentre una città può contenere più righe di persone.

Tornando all’SQL. nell’esempio sopra un esempio di query SQL potrebbe essere:
SELECT *
FROM PERSONE
WHERE NOME = "MARIO"Analizziamo questo codice e quello che fa:
SELECT è il comando standard per richiedere al Database di ritornare delle informazioni. Il valore * dice al database di ritornare tutte le colonne della tabella risultante.
FROM è invece il comando per dire al DATABASE su quali tabelle devono cercare le colonne presenti nella SELECT.
WHERE è la condizione di ricerca e in questo caso la condizione richiesta è: Tutte le righe che hanno contengono nella colonna NOME il valore “MARIO”.
Fin qui sembra semplice ma adesso proviamo qualcosa del genere:
query = "SELECT *
FROM PERSONE WHERE NOME = '" + nomePersona + "'"; Qui siamo su Java, un linguaggio compilato utilizzato per la realizzazione di applicazioni e programmi spesso in ambito aziendale. Se non lo conoscete ci sono molti articoli e tutorial nel sito che ho inserito e inserirò in futuro.
In particolare in questa parte di codice sto istanziando (dando un valore) una variabile Stringa (testo) con all’interno una query alla quale però invece del nome abbiamo concatenato una variabile (una scatola contenete un valore che cambia). Il senso è che in una parte del codice verrà valorizzata quella variabile con un nome (ad esempio chiedendo all’utente di inserire il nome in una finestra o nella riga di comando). Si ci fida che l’utente inserisca proprio il nome, ma se questo non dovesse succedere?.
Se ad esempio invece di un semplice “MARIO” venisse inserito ” ‘ OR 1=1 OR NOME = ‘ ” cosa succederebbe?.
Ad database arriverebbe questa query:
SELECT *
FROM PERSONE
WHERE NOME = '' OR 1 = 1 OR NOME = ''Quel “OR” si chiama operatore logico e significa che basta che una delle due condizioni (quella a destra dell’OR o quella a sinistra) sia logicamente vera allora in automatico sarà tutto vero. Qui la magia: 1 è sempre uguale a se stesso per cui quella condizione vale per ogni riga del database.
Questo significa che questa query restituirà tutto il database, anche i nomi che non hai inserito perché non conoscevi. Immagina ora se in quel database ci fossero il tuo nome utente e la tua password!.
Nel tempo sono uscite molte tecniche utili per evitare questo problema. Una delle più consigliate indipendentemente da tutto quello che vi dirò successivamente è la sanificazione dell’input dell’utente.
Cosa significa sanificare l’input?. Niente che riguardi i germi. Guardate cosa ha inserito l’utente sopra invece del nome. Se notate tutto questo è stato possibile poiché l’utente era libero di inserire caratteri speciali come l’apostrofo (‘).
Infatti se non avesse potuto inserire questo carattere speciale il risultato sarebbe stato:
SELECT *
FROM PERSONE
WHERE NOME = 'OR 1=1 OR NOME = 'Come vedere adesso il colore della parte inserita dall’utente è rossa poiché adesso è considerata una stringa (quindi verrà interpretata come il nome della persona da cercare).
Questo ci aiuta a capire che eliminare i caratteri speciali che SQL potrebbe interpretare come parte della query (come ad esempio l’apostrofo) risolve una parte dei problemi.
Oggi sono stati realizzati dei metodi molto più potenti che aiutano a proteggersi da questi attacchi chiamata SQL Injection (iniezione di codice SQL).
Questi metodi in Java e PHP sono chiamate Prepared Statement e oggi parleremo proprio di queste.
La strategia di difesa è semplice: definire prima la struttura della query e trasmettere i valori separatamente. In PHP, PDO e i prepared statement offrono proprio questo confine. Da soli però non funzionano: identificatori dinamici, credenziali nel codice, messaggi d’errore troppo dettagliati e privilegi eccessivi possono rappresentare vulnerabilità importanti.
Indice
- Che cos’è una SQL injection
- Prepared Statement: cosa sono?
- Esempio minimo vulnerabile
- Utilizzo delle prepared statement
- Liste con in e valori multipli
- Validare input e parti dinamiche
- Errori, segreti e privilegi
- Errori frequenti
- Checklist di code review
Che cos’è una SQL injection
La voce CWE-89 descrive il problema Improper Neutralization of Special Elements used in an SQL Command (‘SQL Injection’). In termini pratici, l’applicazione costruisce una query inserendo direttamente all’interno l’input dell’utente e non è in grado di distinguere la differenza tra comando SQL e il valore inserito. Questo fa si che qualcuno possa inserire nascosti nei valori dei comandi SQL modificando l’effetto della query.
Immaginiamo una ricerca per email. La struttura prevista è stabile: SELECT ... WHERE email = valore. Se il valore viene incollato dentro la stringa SQL, caratteri che hanno un significato speciale nel linguaggio SQL possono alterare quella struttura., Basterà inserire un apostrofo inserito anche innocentemente, come ad esempio nella ricerca di un libro dell’editore O’Reilly.
OWASP (fondazione no-profit per il miglioramento della sicurezza del software) indica come prima difesa contro questo tipo di attacco le prepared statement, ma vediamo cosa sono e come funzionano.
Prepared Statement: cosa sono?
I prepared statement sono un componente che permette di fare eseguire al database una query già pronta e parametrizzata. Questo significa che ti permettono di definire una query e il posto dove andranno inseriti i valori e questi due pezzi vengono inviati al DBMS separatamente.
Il vantaggio dell’uso di questa tecnica e che i valori inseriti dall’utente, anche se contenenti caratteri speciali saranno considerati come valori e no come parte del codice
La validazione stabilisce se il dato è accettabile per l’applicazione; la parametrizzazione impedisce che venga interpretato come SQL. Sono controlli complementari. La sicurezza perimetrale può aiutare ma non sostituisce un codice sicuro e soprattutto una sicura gestione delle query.
Questo modello va applicato a ogni punto di ingresso, non soltanto ai moduli visibili. Parametri di route, richieste JSON, valori letti da API partner e dati recuperati da una coda possono diventare inattendibili. Conviene centralizzare la connessione PDO, mantenere le query vicino al codice che ne definisce i parametri e documentare quali controlli di business precedono ciascuna operazione. La tracciabilità rende più semplice verificare il flusso durante una revisione.
Esempio codice vulnerabile
Questo è un esempio di frammento vulnerabile, nel quale l’input dell’utente che si trova dentro la variabile $email, viene concatenata direttamente alla query tramite l’operatore di concatenamento ‘.’ che crea un’unica stringa di testo. Il fetchAll esegue la stringa come query nel database. Qualsiasi carattere speciale viene concatenato nella stringa e entra a far parte del codice della query.
<?php
// ESEMPIO INTENZIONALMENTE VULNERABILE: solo laboratorio locale isolato.
$pdo = new PDO('sqlite:' . __DIR__ . '/demo.sqlite');
$email = $_GET['email'] ?? '';
// ERRORE: l'input viene concatenato al comando SQL.
$sql = "SELECT id, display_name FROM users WHERE email = '" . $email . "'";
$rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
Utilizzo delle prepared statement
Una connessione PDO può essere configurata per generare eccezioni e preferire i prepared statement nativi quando il driver li supporta:
<?php
declare(strict_types=1);
$dsn = getenv('APP_DSN') ?: 'sqlite:' . __DIR__ . '/demo.sqlite';
$user = getenv('APP_DB_USER') ?: null;
$password = getenv('APP_DB_PASSWORD') ?: null;
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$email = trim((string) ($_GET['email'] ?? ''));
if ($email === '' || strlen($email) > 254 || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
http_response_code(400);
exit('Parametro non valido.');
}
$statement = $pdo->prepare(
'SELECT id, display_name FROM users WHERE email = :email LIMIT 1'
);
$statement->execute(['email' => $email]);
$userRow = $statement->fetch();Nel codice possiamo vedere che il parametro email viene preso da una richiesta GET (all’interno quindi dell’URL, probabilmente come query parameter). L’istruzione if verifica che il parametro sia valido (ovvero non sia vuoto, non sia superiore a 256 caratteri che è il massimo numero di caratteri standard per le email e che sia una email valida).
Viene creata una query dove viene inserito il ferma posto :email. e successivamente la si esegue passando al ferma posto il valore della variabile $email. Nel caso di PHP il manuale consiglia un marker per ogni valore da inserire.
Poi il binding esplicito, ovvero inserendo il tipo di valore che si ci deve aspettare da una leggibilità e sicurezza in più al tuo codice:
$statement = $pdo->prepare(
'SELECT id, title FROM articles WHERE author_id = :author_id AND published = :published'
);
$statement->bindValue(':author_id', $authorId, PDO::PARAM_INT);
$statement->bindValue(':published', true, PDO::PARAM_BOOL);
$statement->execute();Non racchiudere i placeholder nella query tra virgolette (ad esempio WHERE email = ‘:email’) ed è sconsigliato mescolare nella stessa query parametri nominativi e ? (anche essi utilizzabili come segna posto).
Both named and question mark parameter markers cannot be used within the same statement template; only one or the other parameter style. Use these parameters to bind any user-input, do not include the user-input directly in the query.
https://www.php.net/manual/en/pdo.prepare.php
You must include a unique parameter marker for each value you wish to pass in to the statement when you call PDOStatement::execute(). You cannot use a named parameter marker of the same name more than once in a prepared statement, unless emulation mode is on.
Note: Parameter markers can represent a complete data literal only. Neither part of a literal, nor keyword, nor identifier, nor whatever arbitrary query part can be bound using parameters. For example, you cannot bind multiple values to a single parameter in the IN() clause of an SQL statement.
Liste IN e valori multipli
Un singolo placeholder non rappresenta una lista di valori per cui se nella query vorresti invece inserire una lista devi utilizzare un trucchetto. Crea un segnaposto per ogni elemento dopo averlo validato e avere pulito l’array di valori e inserisci poi quei valori nella query. La funzione execute sostituirà ogni valore (seguento l’ordine dell’array) a ogni market ‘?’
$ids = array_values(array_filter(
[12, 18, 27],
static fn (mixed $id): bool => filter_var($id, FILTER_VALIDATE_INT) !== false
));
if ($ids === [] || count($ids) > 50) {
throw new InvalidArgumentException('Lista ID non valida');
}
$marks = implode(',', array_fill(0, count($ids), '?'));
/* Il risultato di implode sata una stringa '?,?,?,? etc.. */
$statement = $pdo->prepare("SELECT id, title FROM articles WHERE id IN ($marks)");
$statement->execute($ids);
Anche qui i valori restano separati dalla query e verranno inglobati come valori direttamente all’interno del database.
Validazione dell’input e parti dinamiche
I placeholder però possono essere utilizzati solo per i valori e non per inserire elementi strutturali nelle query (come ad esempio l’ordinamento). Questa potrebbe essere una vulnerabilità e per risolvere il problema è necessario utilizzare una allow lista, una lista di valori permessi per validare l’input in modo da non inserire direttamente il valore in arrivo se non quello aspettato.Qui sotto un esempio di come si possa inserire nella query un valore di ordinamento tra quelli permessi.
$allowedSort = [
'recenti' => 'created_at DESC',
'titolo' => 'title ASC',
];
$sortKey = (string) ($_GET['sort'] ?? 'recenti');
$orderBy = $allowedSort[$sortKey] ?? $allowedSort['recenti'];
$statement = $pdo->prepare(
"SELECT id, title FROM articles WHERE category_id = :category_id ORDER BY $orderBy LIMIT 20"
);
$statement->execute(['category_id' => $categoryId]);Non copiare mai direttamente $_GET['sort'] nella query. La validazione deve essere positiva: tipo atteso, lunghezza massima, insieme di valori ammessi e regole di business. Utilizzare semplicemente una denylist per eliminare le liste sospette non è una protezione adeguata in questi casi.
Gestione sicura di errori, segreti e privilegi
PDO::ERRMODE_EXCEPTION aiuta a intercettare i problemi e gli errori, per questioni di sicurezza è meglio evitare di dare troppe informazioni all’uomo che vuole attaccarti perciò potrebbe essere più sicuro intercettare gli errori restituiti e restituire solamente un errore generico.
try {
$statement->execute(['email' => $email]);
} catch (PDOException $exception) {
$eventId = bin2hex(random_bytes(8));
error_log("DB event {$eventId}: " . $exception->getMessage());
http_response_code(500);
exit("Errore temporaneo. Riferimento: {$eventId}");
}Altri consigli di sicurezza possono essere: in produzione configura display_errors=Off, proteggi i log e non registrarvi password, token o interi payload sensibili. DSN, utente e password devono provenire da variabili d’ambiente o da un secret manager, non dal repository. Cambia spesso i segreti esposti infatti, una volta committati su git, anche se li cancellassi dalla versione attuale comunque rimarrebbero visibili nella cronologia. L’account usato dall’applicazione deve avere solo i permessi necessari. Un frontend in sola lettura non dovrebbe poter creare tabelle; un servizio che aggiorna profili non dovrebbe amministrare utenti del database. Se possibile separa gli account per componenti e usa viste per restringere righe e colonne. I privilegi minimi non eliminano la vulnerabilità, ma riducono l’impatto di un errore residuo.
Il giusto mindset
- “Uso
prepare(), quindi sono al sicuro.” Se concateni input nel testo prima diprepare(), la query resta vulnerabile. - Eseguire l’escape manualmente. L’escaping dipende da driver, charset e contesto; OWASP lo considera una difesa fragile e di ultima istanza.
- Usare placeholder per identificatori. Colonne e ordinamento richiedono una mappatura allowlist, non il binding.
- Confondere validazione e parametrizzazione. La prima tutela le regole applicative; la seconda separa codice e dati. Servono entrambe.
- Mostrare errori SQL al client. Tabelle, query e stack trace aiutano un aggressore e possono rivelare dati.
- Connettersi come amministratore. Trasforma un singolo difetto in un incidente molto più grave.
- Affidarsi solo a WAF o scanner. Sono livelli aggiuntivi, non sostituti della correzione nel codice.
Checklist di code review
- Ogni valore esterno passa tramite placeholder e
execute()/bindValue(). - Nessun input viene concatenato in
query(),exec()o nel testo diprepare(). - Tabelle, colonne e ordinamenti dinamici provengono da allowlist definite nel codice.
- Tipo, formato, lunghezza e regole di business sono verificati prima della query.
- Le liste
INhanno placeholder individuali e una dimensione massima. - PDO usa eccezioni; gli errori pubblici sono generici e i log sono protetti.
- Credenziali e DSN non sono nel codice o nella cronologia del repository.
- L’utente database ha privilegi minimi e distinti per il caso d’uso.
- I test includono input anomali e verificano che dati e schema restino invariati.
- Dipendenze, PHP e driver database ricevono aggiornamenti di sicurezza supportati.
Conclusione
In questa prima parte abbiamo capito cosa è la SQL injection ed abbiamo affrontato le base di come si risolve in PHP, Ad oggi ci sono framework che spesso integrano già queste funzionalità ma conoscere questi fondamento è comunque importante per imparare a fare bene il proprio lavoro anche quando questi non servono.
Vedremo nella seconda parte come applicare invece gli stessi principi a Java.
Fonti primarie
- PHP Manual, PDO::prepare e Prepared statements and stored procedures.
- OWASP Cheat Sheet Series, SQL Injection Prevention Cheat Sheet.
- MITRE, CWE-89: Improper Neutralization of Special Elements used in an SQL Command.
