La SQL injection è un bug di costruzione delle stringhe
La SQL injection si verifica quando l'input dell'utente finisce dentro il testo SQL che il database analizza. Appena quel confine si confonde, appena un valore digitato dall'utente diventa sintassi eseguita dal database, l'utente può fare tutto ciò che puoi fare tu.
Ecco il classico anti-pattern, in uno pseudocodice che qualsiasi linguaggio può produrre:
-- NON FARLO
query = "SELECT * FROM users WHERE name = '" + user_input + "'"
Se user_input è Ada, ottieni una normale ricerca. Se user_input è ' OR 1=1 --, ottieni:
SELECT * FROM users WHERE name = '' OR 1=1 --'
Il -- commenta l'apice finale, OR 1=1 corrisponde a ogni riga e l'attaccante ha appena scaricato la tua tabella utenti. Le versioni peggiori concatenano ; e un secondo statement per eliminare tabelle, esfiltrare dati o inserire un nuovo account amministratore.
La vulnerabilità non è in SQLite. È nel codice che ha costruito quella stringa.
Query parametrizzate: la vera soluzione
Una query parametrizzata separa il testo SQL dai valori. L'SQL contiene dei segnaposto, ? o :name, e tu passi i valori a parte. SQLite analizza e compila l'SQL una volta, poi collega i tuoi valori nel piano compilato. I valori non possono diventare SQL.
Esegui nel modo sicuro una ricerca che sembra vulnerabile:
Nella shell di SQLite scrivi letteralmente il valore, ma nel codice della tua applicazione l'equivalente è questo (driver sqlite3 di Python):
# Python: parametrizzata, sicura
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))
Passa l'SQL e la tupla di valori come due argomenti separati. Il driver li invia a SQLite separatamente. Anche se user_input è ' OR 1=1 --, SQLite cerca un utente che si chiama letteralmente ' OR 1=1 -- e non ne trova nessuno.
Cosa significa davvero "sicuro" qui
La sicurezza non viene dal riconoscimento di schemi o dall'escape. È strutturale. SQLite compila lo statement in una forma interna prima ancora di vedere il tuo valore:
-- Lo statement compilato ha uno spazio, non una stringa.
SELECT * FROM users WHERE name = ?
^
spazio per il parametro
Quando colleghi un valore, entra in quello spazio come dato tipizzato: TEXT, INTEGER, BLOB o altro. SQLite non lo analizza mai di nuovo come SQL. Non c'è sintassi da iniettare perché il parser ha già finito il suo lavoro.
Ecco perché le query parametrizzate sono affidabili in un modo in cui l'escape non lo sarà mai. L'escape cerca di ripulire una stringa dai caratteri pericolosi. Il binding non costruisce proprio la stringa pericolosa.
Non usare la formattazione di stringhe
Ogni linguaggio ha una scorciatoia allettante (le f-string in Python, i template literal in JavaScript, String.format in Java) e ognuna di queste è un modo per spararsi sui piedi con l'SQL.
# NO: la f-string interpola il valore nel testo SQL
cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'")
# NO: stesso problema, formattazione con %
cursor.execute("SELECT * FROM users WHERE name = '%s'" % user_input)
# SÌ: segnaposto + argomento con i valori
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))
Le prime due inseriscono l'input dell'utente nella stringa SQL prima ancora che il driver la veda. Quando SQLite riceve la query, il danno è fatto. La terza tiene l'SQL e il valore su corsie diverse.
La regola è meccanica: se ti accorgi di costruire una stringa SQL con +, f-string, format o template literal nel punto in cui va un valore, fermati e usa un segnaposto.
Più parametri e segnaposto con nome
Le query reali di solito hanno più di un valore. SQLite supporta sia i segnaposto posizionali ? sia quelli con nome :name:
Nel codice dell'applicazione diventano:
# Posizionali
cursor.execute(
"SELECT * FROM orders WHERE customer = ? AND status = ?",
("Ada", "paid"),
)
# Con nome: più chiaro quando i parametri sono diversi
cursor.execute(
"SELECT * FROM orders WHERE total > :min_total AND status = :status",
{"min_total": 50, "status": "paid"},
)
I parametri con nome scalano meglio. Oltre i tre o quattro valori, ?, ?, ?, ? diventa un gioco a indovinare; :customer, :total, :status, :created_at si documenta da solo.
Gli identificatori richiedono un approccio diverso
I parametri collegati funzionano solo per i valori: ciò che sta a destra di =, dentro IN (...), in VALUES (...). Non funzionano per nomi di tabella, nomi di colonna o parole chiave SQL come ASC/DESC.
-- Questo NON funziona. Il segnaposto non può sostituire un nome di colonna.
SELECT * FROM users ORDER BY ? ASC
Se ti serve un identificatore dinamico, per esempio per far scegliere all'utente la colonna su cui ordinare, confrontalo con una allowlist prima di costruire l'SQL:
# Approccio con allowlist
ALLOWED_SORT_COLUMNS = {"name", "created_at", "role"}
if sort_column not in ALLOWED_SORT_COLUMNS:
raise ValueError(f"Colonna di ordinamento non valida: {sort_column}")
query = f"SELECT * FROM users ORDER BY {sort_column} ASC"
cursor.execute(query)
La stringa fornita dall'utente viene confrontata con un insieme fisso di valori sicuri prima di avvicinarsi all'SQL. La f-string è accettabile qui solo perché sort_column ormai non può essere altro che uno dei tre nomi scritti nel codice.
Un tentativo di injection concreto, disinnescato
Vediamo le due versioni fianco a fianco con un input ostile. Crea una piccola tabella utenti:
La forma vulnerabile restituisce tutti gli utenti. La forma parametrizzata cerca un utente che si chiama letteralmente ' OR 1=1 -- e non restituisce niente. Stesso input, risultato completamente diverso: nel secondo caso il valore non è mai diventato SQL.
Una breve checklist
- Usa i segnaposto
?o:nameper ogni valore che arriva da fuori dal tuo codice: input dell'utente, corpi delle richieste, variabili d'ambiente, qualsiasi cosa non hai scritto tu direttamente. - Non costruire mai SQL con
+, f-string oformatnel punto in cui va un valore. - Per nomi di tabella o colonna dinamici, confrontali con una allowlist fissa prima di inserirli nella query.
- Fidati del driver. Non scrivere la tua funzione di escape degli apici. Il meccanismo dei parametri collegati è più vecchio, più collaudato e corretto.
- Rivedi le query del tuo team con una sola domanda: c'è input dell'utente concatenato nel testo SQL? Se sì, correggilo.
Fai tua questa abitudine e la SQL injection smette di essere una categoria di bug a cui devi pensare.
Prossimo passo: connettersi dalle applicazioni
Hai visto la forma sicura di una query: segnaposto nell'SQL, valore passato a parte. La prossima pagina mostra come collegare davvero SQLite dal codice di un'applicazione reale in Python, Node.js e qualche altro linguaggio, compresa la gestione delle connessioni e il ruolo delle query parametrizzate in un tipico flusso di richiesta.
Domande frequenti
SQLite è vulnerabile alla SQL injection?
Sì. SQLite è vulnerabile quanto qualsiasi altro database SQL quando il codice dell'applicazione costruisce le query concatenando stringhe. La soluzione non è un'impostazione di SQLite: è il modo in cui passi i valori dalla tua applicazione. Usa query parametrizzate con i segnaposto ? o :name e il driver li gestisce in sicurezza.
Come fanno le query parametrizzate a prevenire la SQL injection?
Quando usi segnaposto come ?, SQLite analizza e compila prima la query, poi collega i tuoi valori negli spazi dello statement già compilato. I valori non possono mai diventare sintassi SQL: vengono trattati come dati, punto. Non c'è nessuna stringa da cui un attaccante possa evadere.
Non posso semplicemente fare l'escape degli apici nell'input dell'utente?
No. L'escape manuale è fragile: prima o poi ti sfugge un caso limite (apici Unicode, trucchi di codifica, marcatori di commento) e metti in produzione una vulnerabilità. I driver offrono i parametri ? e :name proprio perché tu non debba pensare all'escape. Usali sempre, anche per i valori che 'sai' essere sicuri.
E i nomi di tabelle o colonne che arrivano dall'input dell'utente?
I parametri collegati funzionano solo per i valori, non per gli identificatori. Se un nome di tabella o di colonna deve essere dinamico, confrontalo con una allowlist di nomi noti prima di inserirlo nell'SQL. Non passare mai un identificatore fornito dall'utente così com'è attraverso la formattazione di stringhe.