Qualsiasi istruzione SQL costruita unendo stringhe con valori non attendibili può essere oggetto di injection, e la soluzione è la stessa in ogni linguaggio: mantenere l'SQL fisso e vincolare i valori come parametri. L'OWASP Query Parameterization Cheat Sheet classifica la SQL injection sotto A05:2025-Injection nella OWASP Top 10:2025 e la definisce una delle vulnerabilità web più pericolose.
Perché la concatenazione fallisce
SQL non distingue davvero tra piano di controllo e piano dei dati. Un attaccante inserisce un meta carattere in un input e compaiono comandi SQL dove prima non ce n'erano. La pagina della community OWASP sulla SQL injection mostra una query C# costruita per concatenazione:
string query = "SELECT * FROM items WHERE owner = '"
+ userName + "' AND itemname = '"
+ ItemName.Text + "'";
Se l'attaccante inserisce name' OR 'a'='a come nome dell'elemento, la condizione risulta sempre vera. La query restituisce tutte le righe della tabella items, indipendentemente dal proprietario. A seconda delle funzionalità e dei privilegi del database, lo stesso difetto può anche modificare dati, incidere sui file o eseguire comandi del sistema operativo.
L'origine del valore non ha importanza. I controlli si applicano sia che il dato provenga da una richiesta HTTP, da un altro servizio o da dati memorizzati in precedenza.
Trovare le query costruite con stringhe
Si parte con un grep per le parole chiave SQL vicine a concatenazione o formattazione. Questi pattern sono una prima passata grossolana, non uno scanner. Producono molto rumore e non intercettano le query spezzate su più righe.
# concatenation (Java, C#, JavaScript, PHP uses "." instead of "+")
grep -rnEi "(select|insert into|update|delete from)[^;]*[\"'] *\+" src/
# Python f-strings and .format()
grep -rnEi "f[\"'](select|insert|update|delete)|(select|insert|update|delete).*\.format\(" .
# JavaScript template literals
grep -rnEi '`(select|insert|update|delete)[^`]*\$\{' src/
# sprintf-style formatting
grep -rnEi "sprintf\(.*(select|insert|update|delete)" .
Si legga ogni risultato e si verifichi che i valori non attendibili siano vincolati e non concatenati. Poi si guardi nei punti che un grep tende a saltare:
- Stringhe di query degli ORM. Le query Hibernate possono essere vulnerabili quando i valori vengono concatenati nella stringa della query invece di essere vincolati.
- Stored procedure. Mantengono l'SQL nel database, ma non sono sicure in automatico. OWASP mostra una procedura PL/SQL che unisce
usreitmin una stringa di query e afferma che è "vulnerable to the same SQL injection attack shown in the first example". Vincolare gli argomenti della chiamata protegge la chiamata, non l'SQL dinamico costruito all'interno della procedura. - Parametrizzazione lato client. Molte librerie lato client costruiscono le query per concatenazione di stringhe e inviano al server la query grezza. Si verifichi che la parametrizzazione avvenga lato server.
- Identificatori dinamici e direzioni di ordinamento. Non possono essere vincolati, ed è qui che la concatenazione sopravvive a una revisione sulla parametrizzazione.
Poi si testa. L'OWASP Web Security Testing Guide contiene test di SQL injection per il testing manuale e automatizzato, comprese le tecniche blind e out-of-band. Per una verifica esterna di ciò che la revisione ha lasciato sfuggire, serve un penetration test.
Sostituirle con parametri
Si definisce la struttura SQL separatamente dai valori dei dati, poi si vincolano i valori tramite l'API del database. Questo è l'esempio dell'OWASP SQL Injection Prevention Cheat Sheet, senza pulizia delle risorse e gestione degli errori:
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, customerName);
ResultSet results = statement.executeQuery();
Un valore come tom' or '1'='1 viene ora trattato come il nome utente letterale da cercare. Concatenare un valore non attendibile nella query prima di prepararla annulla la protezione.
Con un ORM, il binding va fatto anche nella stringa della query. Gli esempi OWASP usano un parametro con nome in HQL, FROM Inventory i WHERE i.productDescription = :productDescription, e passano il valore con setParameter.
Per l'SQL dinamico all'interno delle stored procedure si usano le variabili di bind. In Oracle PL/SQL significa EXECUTE IMMEDIATE con USING. In SQL Server significa sp_executesql con un elenco di parametri dichiarato. Le procedure senza SQL dinamico non richiedono nulla di particolare, perché i loro parametri sono già vincolati.
Il binding impedisce a un valore di alterare la sintassi SQL. Non stabilisce se chi chiama possa accedere all'account corrispondente, quindi i controlli di autorizzazione restano al loro posto.
Cosa i parametri non possono coprire
I segnaposto dei parametri rappresentano valori di dati. Non possono rappresentare nomi di tabelle, nomi di colonne o parole chiave come ASC e DESC. Quando gli utenti devono scegliere, si associa la selezione a un piccolo insieme di token controllati dall'applicazione e si rifiuta tutto ciò che non è riconosciuto. Prima:
String query = "SELECT id, name FROM items ORDER BY name " + sortParam;
Dopo:
Map<String, String> directions = Map.of("asc", "ASC", "desc", "DESC");
String direction = directions.get(sortParam);
if (direction == null) {
throw new IllegalArgumentException("Unknown sort direction");
}
String query = "SELECT id, name FROM items ORDER BY name " + direction;
Il testo aggiunto alla query è ora sempre uno di due letterali, mai ciò che l'utente ha inviato. Si continui a vincolare ogni valore di dati nella stessa query, anche quando supera i controlli di tipo, lunghezza o formato.
Non si ripieghi sull'escaping manuale. Dipende dal dialetto SQL e dalla configurazione: la modalità NO_BACKSLASH_ESCAPES di MySQL, per esempio, disattiva l'escaping con backslash. OWASP indica l'escaping come fortemente sconsigliato e descrive le deny list come piene di scappatoie.
Limitare i danni
Il principio del privilegio minimo non previene la SQL injection. Limita ciò che un attaccante può fare tramite un account del database, quindi va applicato insieme ai parametri.
- Si parta da zero e si aggiungano i diritti necessari a ciascun account applicativo. Capire cosa togliere da un'ampia concessione di privilegi è il modo in cui i diritti vengono dimenticati.
- Dare a un account applicativo accesso da DBA o amministratore è la scorciatoia più comune, perché tutto "funziona e basta". OWASP lo definisce molto pericoloso.
- Una pagina di login ha bisogno dell'accesso in lettura ai campi nome utente e password e di nessun accesso in scrittura. Una pagina di registrazione ha bisogno dell'insert. Questa separazione è applicabile solo se le due usano utenti di database diversi, quindi si assegni a ogni applicazione web il proprio.
- Gli attaccanti non hanno bisogno dell'injection per abusare di un account con privilegi eccessivi. Possono cambiare un parametro da un valore legittimo a uno per cui non sono autorizzati ma l'applicazione sì, ed è per questo che concessioni ristrette aiutano anche quando le query sono pulite.
- Una vista può esporre solo i campi necessari a un account, con
SELECTconcesso sulla vista e non sulla tabella. Si controllino prima le regole del proprio database: PostgreSQL normalmente verifica l'accesso alle tabelle sottostanti con i privilegi del proprietario della vista. Le query sulle viste richiedono comunque parametri. - Conta anche l'account del sistema operativo. Non si esegua il DBMS come root o system. OWASP osserva che MySQL su Windows per impostazione predefinita viene eseguito come system.
Da dove iniziare
- Si elenchi ogni punto in cui viene costruito SQL: codice applicativo, stringhe di query degli ORM e stored procedure che usano SQL dinamico. Si eseguano come prima passata i pattern grep illustrati sopra.
- Si sostituiscano i valori concatenati con parametri vincolati, lato server, e si ricostruisca l'SQL dinamico all'interno delle procedure con variabili di bind.
- Si sostituiscano le scelte dinamiche di tabelle, colonne e ordinamento con mappature fisse che rifiutano input sconosciuti.
- Si verifichino gli account del database. Si tolgano i diritti di DBA e amministratore agli account applicativi, si assegni a ogni applicazione il proprio utente e si sposti l'accesso in sola lettura su concessioni ristrette o viste.
- Si eseguano sul risultato i test di SQL injection della Web Security Testing Guide, oppure si chieda al nostro team di sviluppo software sicuro di rivedere le modifiche.
