Quando il Database ti ascolta troppo: un Viaggio dentro la SQL Injection

C’è un momento, nello sviluppo di un’applicazione, in cui tutto sembra filare liscio: la form di login funziona, i prodotti si caricano velocemente, il database risponde obbediente a ogni richiesta.
È in quei momenti di apparente tranquillità che nasce una delle vulnerabilità più vecchie del web — e ancora oggi una delle più presenti nei report di sicurezza.

La SQL Injection non ha niente a che fare con attacchi da film, né un’operazioni segrete degne di thriller informatici.
È, prima di tutto, un errore umano, un fraintendimento.
Un momento in cui un’app decide di credere troppo all’input ricevuto.

Questo vuole essere un viaggio dentro quell’incomprensione.

1. L’origine silenziosa dell’errore

Immagina il backend come una cucina.
Il database è il cuoco: preciso, veloce, obbediente.
Il tuo codice è il cameriere che porta i comandi.

Se il cameriere — cioè l’applicazione — scrive “Porta una pizza ai funghi”, il cuoco cucina una pizza ai funghi.
Se scrive “Porta una pizza e poi chiudi il ristorante”, il cuoco… proverà a farlo.
Non perché sia malintenzionato, ma piuttosto perché è stato addestrato a eseguire comandi, non a interpretarli.

Nelle applicazioni web avviene lo stesso: il database non “capisce” ciò che è innocuo e ciò che è potenzialmente distruttivo. Esegue e basta.

La SQL Injection nasce precisamente qui:
nel momento in cui un dato fornito dall’utente viene scambiato per un comando.

2. La dinamica invisibile: come nasce una SQLi

Considera una query preparata nel modo più ingenuo possibile:

SELECT * FROM utenti WHERE email = 'VALORE_INSERITO_DALL_UTENTE';

Per chi legge è solo una stringa, nulla di speciale.

Per il database, quella stringa contiene istruzioni.

Finché dentro ci finisce un input valido, non succede nulla.
Ma quando la distinzione tra dato e codice SQL si sfuma, l’equilibrio si rompe.
Il parser del DBMS, che non ha un concetto di “malizia”, eseguirà tutto ciò che rientra nella grammatica SQL.

È questo il cuore della SQL Injection: il database esegue cose che non avrebbero mai dovuto essere eseguite.

“SELECT * FROM utenti WHERE email =”

Questa è la parte legittima della query.
Stai dicendo al database:

“Prendi tutti gli utenti la cui email è uguale a QUALCOSA.”

Fin qui tutto bene.
Il database capisce perfettamente cosa deve fare.

'VALORE_INSERITO_DALL_UTENTE'

Ed ecco il punto delicato: il valore dell’utente viene inserito direttamente dentro la query, dentro la stringa SQL.

Narrativamente, è come se stessi scrivendo una lettera al database del tipo:

“Ecco la richiesta… e dentro ci ho incollato anche quello che ha detto qualcuno da fuori”.

Il problema non è il valore in sé, ma dove si trova: è stato incollato nella parte di testo che il database interpreta come codice SQL.

Il database, a questo punto, fa quello che sa fare: legge tutto ciò che è tra gli apici come parte della stessa istruzione.

Non ha modo di distinguere:

  • cosa volevi tu (codice “buono”),
  • da cosa arriva dall’esterno (input utente).

È come dare al database una frase con un pezzo scritto da qualcun altro… ma senza dirgli chi ha scritto cosa.

Per il database, è tutto un pezzo unico

Il DBMS non possiede un sistema di “intenzioni” o “morale”.
Non pensa: “Questo valore arriva da un form, quindi lo tratto diversamente.”

Lui vede solo una stringa SQL completa e ciò che è tra gli apici diventa parte integrante della logica della query, perché si trova nella stessa frase, senza separazioni tecniche.

Perché questa semplice linea apre la porta alla SQL Injection

Quando inserisci direttamente l’input dentro la query, stai facendo due cose:

  1. Dando il controllo di una parte della query a una persona esterna
  2. Lasciando che il database interpreti quella parte come codice SQL valido

È come fornire il testo della ricetta e permettere a qualcun altro di scrivere ingredienti e istruzioni direttamente sulla stessa pagina.
Il cuoco non saprà distinguere chi ha scritto cosa: eseguirà comunque.

La SQL Injection nasce da qui: dalla totale mancanza di separazione tra codice e dati.

Ricapitolando per il lettore principiante

  • Questa query è pericolosa non per quello che fa, ma per come viene costruita.
  • Inserire direttamente l’input dell’utente dentro la query significa: lasciare che l’utente modifichi la logica dell’istruzione SQL.
  • Il database non distingue tra “dato innocuo” e “istruzione”: vede solo sintassi.
  • Ecco perché servono i prepared statement, che separano correttamente i due livelli.

3. Scene dal mondo reale (raccontate, non replicate)

Form di login: la fiducia mal riposta

Le autenticazioni basate su query testuali concatenate sono state protagoniste di alcuni dei casi più famosi.
Codice scritto velocemente, magari da diversi sviluppatori che si sono susseguiti nel tempo, senza pensare che la form di login è un punto di contatto diretto tra l’esterno e il database.

Motori di ricerca interni

Nelle funzioni dove l’utente può filtrare, ordinare e cercare, spesso nascono query costruite dinamicamente.
È un terreno fertile per errori — soprattutto nelle app nate anni fa, dove il codice moderno convive ibridamente con soluzioni preistoriche.

Pannelli di amministrazione “di fortuna”

Molte vulnerabilità non nascono in homepage, ma negli strumenti usati dal team IT, nascosti e mai aggiornati.
Piccoli pannelli creati per comodità che, negli anni, diventano reliquie con difese poco solide.

Gli attacchi reali non hanno nulla di sofisticato: sfruttano proprio queste fratture.

4. Cosa succede davvero dentro il database

Il processo è sorprendentemente semplice:

  1. L’app crea una stringa SQL basata su input esterno.
  2. Il database riceve la stringa e la tokenizza.
  3. Se dentro quella stringa trova costrutti grammaticalmente validi, li esegue come istruzioni.

È tutto qui.
La SQLi non sfrutta un bug del database, ma la logica stessa con cui esso interpreta le query.

Per il DB:

  • 'ciao' è una stringa
  • email = 'ciao' è un’istruzione
  • e tutto ciò che viene dopo, purché formalmente valido, è parte della stessa richiesta.

Non riconosce “l’intenzione”; riconosce la sintassi.
Questo è il motivo per cui prevenire la SQL Injection è una questione di disciplina più che di tecnicismi.

Per capirci meglio, quando il database riceve una query, prima la analizza e ne costruisce un query plan: una specie di ricetta ottimizzata per trovare i dati.

Se l’input dell’utente finisce nella parte “logica” della query, questa ricetta cambia.
Ecco perché una SQL Injection può alterare il comportamento dell’intero comando.

🛈 Una nota importante sulle varianti meno visibili
Non tutte le SQL Injection sono “dirette” o producono subito un effetto evidente.
Esistono:

  • Blind SQLi, dove l’attaccante non vede mai il risultato della query, ma capisce cosa succede osservando tempi di risposta o errori.
  • Second-order SQLi, dove l’input malevolo viene salvato in un database e causa la vulnerabilità solo quando un’altra parte dell’applicazione lo usa in una query successiva.

Non servono a capire gli attacchi in dettaglio, ma aiutano a ricordare che una SQLi può emergere anche quando sembra che tutto sia sotto controllo.

5. Difendersi: il vero cuore del problema

5.1 Prepared Statements: la barriera più efficace

Il concetto è semplice e rivoluzionario:
invece di mandare al database una query completa di dati, gli si invia:

  1. una struttura della query con dei segnaposto
  2. i valori separati

Il DB riceve due cose distinte e non può mai confonderle.

Esempio sicuro e corretto:

query = "SELECT * FROM utenti WHERE email = ?"
db.execute(query, [email_input])

Non importa cosa contenga email_input:
il driver la tratterà sempre come dato, mai come codice.

Questa tecnica non è un “consiglio”, è uno standard.

query = "SELECT * FROM utenti WHERE email = ?"

Qui stiamo definendo la struttura della query.
Non stiamo ancora usando dati dell’utente: stiamo solo preparando il “modello” che useremo.

Il pezzo importante è il ?.
Quello non è un errore di sintassi, e non è un carattere casuale: è un placeholder, un segnaposto, un buco lasciato volutamente vuoto.

È come scrivere:

“Caro database, voglio tutti gli utenti con una certa email. Ti mando prima la forma della richiesta… poi ti dirò qual è l’email.”

In questo momento il database non può eseguire nulla di pericoloso, perché non sa ancora quale valore finirà lì dentro.
E soprattutto: non può interpretare il valore come un pezzo di codice, perché non glielo abbiamo ancora dato.

db.execute(query, [email_input])

Questa è la chiamata in cui avviene la parte cruciale della sicurezza.

Qui stiamo passando due cose separate al driver del database:

  1. La struttura della query (query)
  2. I valori da inserire nei placeholder ([email_input])

Il valore email_input si trova dentro una lista/array perché:

  • potresti avere più placeholder (?)
  • il driver vuole un contenitore uniforme (una lista di valori)

Ma la cosa più importante è questa:

👉 Il valore dell’utente non viene mai incollato alla query. Viene passato come dato.

Il database lo riceve in un canale separato e gli assegna automaticamente il ruolo di valore, non di istruzione SQL.
Non importa cosa contenga: sarà sempre trattato come informazione da confrontare, non come codice da eseguire.

Narrativamente, è come mandare al ristorante un ordine diviso in due parti:

  • Foglio 1: “Prepara una pizza, ecco la ricetta.”
  • Foglio 2: “L’ingrediente segreto è: funghi.”

Non importa cosa ci sia scritto nel secondo foglio: il cuoco lo userà come ingrediente, non come un nuovo comando (“chiudi il ristorante”, “butta tutto”, ecc.).
Ecco perché il database non può “fraintendere” l’intento.

Cosa deve ricordare un principiante

Il punto interrogativo (?) è il modo più semplice per dire: “Qui ci va un valore, ma non inserirlo nella stringa.”

L’utente non modifica la query: fornisce dati, non comandi.

I prepared statement sono la linea di difesa più solida contro la SQL Injection.

🛈 Un dettaglio tecnico utile
La maggior parte dei driver moderni invia i prepared statement direttamente al database (server-side prepared statements).
In pochi casi, librerie vecchie simulano il binding lato applicazione, creando la query finale prima di inviarla (client-side binding).

Quasi tutti i framework attuali usano il modello sicuro (lato server), ma è sempre una buona idea verificare il comportamento del driver che si utilizza.

5.2 Validazione dell’input: utile, ma non risolutiva

Molti sviluppatori pensano:
“Basta filtrare l’input.”

In realtà:

  • validare formati è buona pratica (email, integer, range)
  • questo però non impedisce a un input formalmente valido di contenere qualcosa che il DB interpreterebbe in modo diverso dal previsto

Validare serve per la qualità del dato, non per la sicurezza della query.

5.3 ORM e Query Builder: amici, ma solo se li usi bene

Framework moderni (Django, Laravel, SQLAlchemy, etc.) astraggono le query e spesso implementano il parameter binding automaticamente.

Perfetto, ma ricordati che ogni ORM ha la sua “porta laterale”, ovvero le raw queries.

Ecco dove ritorniamo al punto di partenza:
se concateno stringhe per costruire una query, è irrilevante farlo dentro o fuori un framework.

La regola resta la stessa:
mai interpolare input in una query SQL.

🛈 Anche gli ORM hanno angoli difficili
Funzioni avanzate come ricerche full-text, filtri JSON o condizioni costruite dinamicamente possono richiedere stringhe SQL personalizzate.
In questi casi, sei di nuovo nella zona in cui può nascere una SQL Injection.
La regola resta sempre valida: evitare concatenazioni e usare le API di binding offerte dal framework.

5.4 Principio del minimo privilegio

Non si difende solo con il codice, anche il database deve collaborare:

  • evitare utenti DB con privilegi amministrativi
  • limitare tabelle e colonne accessibili
  • revocare comandi non necessari (DROP, ALTER, GRANT)

Se la query malformata colpisce, l’impatto deve comunque essere contenuto.

5.5 Logging, monitoraggio e “segnali deboli”

Una SQL Injection non si verifica sempre con effetti eclatanti, spesso inizia come:

  • un errore inaspettato nei log
  • una ricerca strana nel campo “email”
  • una sequenza di richieste quasi identiche

Monitorare questi comportamenti permette di individuare tentativi prima che diventino incidenti reali.

🛈 Anche gli errori parlano (troppo)
Gli errori SQL dettagliati non dovrebbero mai essere mostrati agli utenti finali.
Un messaggio di errore può rivelare:

  • nomi di tabelle
  • struttura delle colonne
  • tipo di database
  • sintassi di una query interna

Logga tutto, ma mostra all’esterno solo errori generici.

6. Codice sicuro: alcuni esempi universali

Non pensare ai payload, o agli exploit, concentrarti sulle buone pratiche.

Esempio con placeholder

query = "SELECT * FROM prodotti WHERE categoria = ?"
db.execute(query, [categoria_input])

query = "SELECT * FROM prodotti WHERE categoria = ?"
Qui stiamo creando la “forma” della query.
Nota quel punto interrogativo: è il placeholder, il segnaposto, una specie di buco dove poi verrà inserito un valore.
Non stiamo ancora mettendo dentro nessun dato dell’utente: stiamo solo definendo la struttura della richiesta al database.
È come dire: “Caro database, preparati a filtrare i prodotti per categoria. Ti dico già come farlo, poi ti mando la categoria”.

db.execute(query, [categoria_input])
Questa è la parte in cui spediamo sia la query che i valori.
Importante: li inviamo separati.
categoria_input è dentro una lista/array perché molti driver possono ricevere più valori (uno per ogni placeholder).
Il database tratta quel valore come dato, non come parte dell’istruzione SQL, quindi non può trasformarsi in codice.

Messaggio chiave per il principiante: il database vede due pacchetti distinti:

  1. la struttura della query
  2. un elenco di valori da inserire nei buchi

E non li mescolerà mai.

Esempio usando un ORM

// Node.js – esempio generico
const prodotti = await db.prodotti.findAll({
  where: { categoria: categoriaInput }
});

db.prodotti.findAll({...})
Qui non stai parlando direttamente con il database.
Stai chiedendo all’ORM (Object–Relational Mapper) di farlo per te.
L’ORM si occupa di costruire la query SQL in modo sicuro, automaticamente.

where: { categoria: categoriaInput }
Questa parte dice all’ORM: “Voglio tutti i prodotti dove la categoria è uguale al valore inserito dall’utente.”
Ma attenzione: non stai costruendo una stringa.
Stai passando un oggetto, una struttura dati.
Questo dice all’ORM che “categoriaInput” è un valore e non un pezzo di codice.
Sarà l’ORM stesso a predisporre i prepared statement.

const prodotti = await ...
Infine aspetti la risposta del database.
Riceverai un elenco di elementi già controllati dall’ORM, senza mai toccare una query grezza.

Messaggio chiave per il principiante:
Gli ORM fanno il lavoro sporco per te — ma solo se usi la loro API, non se torni a concatenare stringhe manualmente.

Esempio con validazione di tipo

if not isinstance(prezzo_min, int):
    raise ValueError("Formato non valido.")

query = "SELECT * FROM articoli WHERE prezzo >= ?"
db.execute(query, [prezzo_min])

Tutto ciò che va nella query deve passare attraverso un meccanismo che lo tratta come valore, non come istruzione.

  • if not isinstance(prezzo_min, int):
    Prima ancora di parlare con il database, controlliamo che il valore sia del tipo giusto.
    Qui stiamo dicendo: “Prezzo minimo deve essere un numero intero.”
    È una forma di igiene dell’input: non impedisce da sola una SQL Injection, ma elimina input impossibili o malformati.
  • raise ValueError("Formato non valido.")
    Se qualcuno invia qualcosa che non è un numero (es. una stringa inesistente), blocchiamo tutto e informiamo l’applicazione.
    Nessuna query parte e quindi non c’è possibilità che il database debba interpretare cose strane.
  • query = "SELECT * FROM articoli WHERE prezzo >= ?"
    Creiamo la struttura della query con un placeholder: esattamente come nell’esempio iniziale.
  • db.execute(query, [prezzo_min])
    Eseguiamo in modo sicuro la query, passando il numero come valore separato.

Messaggio chiave per il principiante:
La validazione non sostituisce i prepared statement, ma rende i dati più puliti, più prevedibili, e l’app più robusta.

🌟 Ricapitolando per chi inizia da zero

  • Il placeholder (?) è un buco che il database riempie con dati, non con codice.
  • Un ORM costruisce la query in modo sicuro al posto tuo, se usi le sue API.
  • La validazione dei tipi assicura che gli input siano sensati, ma non difende da sola la query.
  • La sicurezza nasce dalla separazione tra la struttura della query e i valori.

🛈 Nota sui placeholder
Alcuni componenti della query — come ORDER BY, LIMIT, o nomi di colonne — spesso non accettano placeholder nei diversi DBMS.
In questi casi occorre usare:

  • un elenco di valori consentiti (whitelist)
  • mapping espliciti tra input dell’utente e valori SQL fissi

Anche qui vale la stessa regola: mai inserire direttamente l’input nella query.

7. Checklist finale: il manifesto anti-SQLi

Un promemoria semplice, ma fondamentale:

  • ❗ Mai concatenare stringhe per creare query
  • ✔ Usare sempre prepared statements
  • ✔ Validare formati e tipi di dati
  • ✔ Limitare i privilegi dell’utente DB
  • ✔ Evitare query raw in framework moderni
  • ✔ Monitorare input sospetti e errori SQL insoliti
  • ✔ Revisionare periodicamente le aree legacy dell’app

La sicurezza è un miglioramento continuo, non un interruttore.

8. Epilogo: quando il database smette di fraintendere

La SQL Injection non è una vulnerabilità “da principianti”.
Non perché sia complessa — anzi, è disarmante nella sua semplicità —
ma perché colpisce chiunque dia per scontato che “quel pezzo di codice è lì da anni e ha sempre funzionato”.

La buona notizia è che la prevenzione è altrettanto semplice: una separazione chiara tra ciò che l’utente dice e ciò che il database esegue.

Una volta costruita quella barriera, il tuo database non ascolterà più “troppo”.
E l’applicazione sarà finalmente libera da uno dei fraintendimenti più pericolosi del web.

📚 Libri consigliati sul tema sicurezza informatica / web security

📖 Per chi inizia / vuole una base semplice

📘 Testi brevi o compatti — per ripasso o consultazione rapida

🛠️ Per sviluppatori / chi lavora su applicazioni web

Questo nodo della rete è alimentato da conoscenza libera e caffeina.
Se hai trovato qualcosa di utile, puoi supportarci con un caffè digitale.
👉 Offrimi un caffè

Powered by Buttondown

Lascia un commento