Sviluppo web
SQL injection: prevenzione che regge
La SQL injection si previene con query parametrizzate, identificatori presi da elenchi chiusi e un utente database con permessi minimi. Cosa resta scoperto anche dopo.
In sintesi
- Il difetto è la stringa costruita a mano, non il testo che l'utente digita.
- Nome di colonna, direzione di ordinamento e nome di tabella non sono valori: si scelgono da un elenco chiuso.
- L'utente database dell'applicazione non deve poter leggere tabelle che non usa né eseguire DDL.
- Il catalogo ha 32 blocchi form e 34 login su 1.556 blocchi: sono le superfici che mandano dati a un database.
La SQL injection si previene con query parametrizzate e un utente database con permessi minimi. Il difetto è la stringa costruita a mano, non il testo che l'utente digita: se il valore e il comando viaggiano separati, il database non ha modo di interpretare il valore come SQL.
Il caso classico non è un attacco esotico. Un campo di ricerca, un ordinamento per colonna, un filtro «solo attivi»: ognuno dei tre punti, scritto con una concatenazione, apre la strada a leggere o cancellare tabelle che il form non nominava. Chi cerca solo nel corpo della POST lascia fuori header, cookie e parametri di ordinamento.
Query parametrizzate, non stringhe pulite
Il driver manda il testo della query e i valori separati al server, e il database non interpreta mai un valore come comando. La stringa ' OR 1=1 -- diventa un testo da cercare, non una condizione. Il meccanismo è lo stesso in ogni linguaggio: $1 in PostgreSQL, un punto interrogativo in MySQL, un segnaposto nominato in SQLite. Le funzioni di escape manuale sono una seconda scelta: bastano una codifica sbagliata o un set di caratteri diverso e il filtro salta.
I punti in cui il parametro non arriva
Minimo privilegio: la seconda metà della difesa
Una query parametrizzata protegge i valori. Un utente database che può leggere l'intera tabella utenti resta un problema quando una query sbagliata espone una colonna in più. L'account dell'applicazione riceve solo i permessi che le servono: SELECT, INSERT e UPDATE sulle tabelle giuste, nessun DDL e nessun accesso al catalogo di sistema. Le migrazioni usano un secondo account, attivo solo durante il deploy.
L'input che non arriva dal form
Header, cookie, parametri di ordinamento, webhook, file importati e job pianificati portano lo stesso rischio di un campo di testo. Un filtro applicato al solo corpo della POST copre metà delle superfici. La regola non cambia: il dato non si valida per fiducia, si passa come parametro, e la validazione dei tipi resta un controllo in più, non una difesa.
Errori e log
Un errore del database non torna al client: il messaggio di sintassi dice all'attaccante quale frammento ha funzionato e quale no. Rispondi 500 con un identificativo di richiesta e cerca il dettaglio nei log. Il formato standard per l'errore verso il client, con un tipo e un'istanza, è descritto in progettare un'API.
Il controllo che puoi mettere in CI
Un grep non è un test di sicurezza, ma intercetta la regressione più comune: la query costruita per concatenazione o per interpolazione di stringa. Il comando sotto cerca le forme sospette nei file del progetto e verifica che l'utente dell'applicazione non sia un superutente. Le due righe non sostituiscono una revisione: servono a impedire che il difetto rientri dopo essere stato corretto.
# query costruite per concatenazione o interpolazione: da rivedere una per una
rg -n "SELECT .* FROM .*\\+" --glob "*.{js,ts,py,php}" src/
# l'utente dell'applicazione non deve essere superutente
psql "$DATABASE_URL" -c "select current_user, usesuper from pg_user where usename = current_user"
Il limite da dichiarare
Nessun blocco del catalogo mette in sicurezza un server: la prevenzione sta nelle query e nei permessi, e un componente React non tocca nessuno dei due. Un web application firewall riduce il rumore degli scanner, ma non sostituisce la parametrizzazione e non intercetta una query raw scritta male. Resta un limite anche sul catalogo: la licenza è dichiarata dal publisher e va verificata sul singolo blocco prima di pubblicarlo.
Tre record di catalogo
La categoria login raccoglie 34 blocchi con campo email e password. La categoria form ne ha 32, dai moduli di contatto a quelli multi-campo. I componenti table coprono la vista lato operatore, dove un errore di filtro si nota subito. L'anteprima è gratuita; il codice completo sta nel piano Pro.
Sul lato autenticazione il middleware non è un controllo sufficiente da solo: leggi middleware di autenticazione, sessione e cookie e il confronto tra server actions e API.
Domande frequenti
Bastano le query parametrizzate per fermare la SQL injection?
Fermano la parte che riguarda i valori, che è la maggioranza dei casi. Restano scoperti gli identificatori costruiti a runtime, come nome di colonna e direzione di ordinamento, e i permessi troppo larghi dell'utente database. Servono tutti e tre i controlli.
Un ORM elimina il rischio?
No. Un ORM parametrizza le query che genera, ma ogni metodo per SQL raw accetta una stringa e riapre il problema. Anche i frammenti di query composti a mano dentro l'ORM vanno parametrizzati.
Come si passa un nome di colonna in ORDER BY?
Con una lista chiusa di valori ammessi, per esempio un dizionario che mappa «nome» sulla colonna reale. Il nome che arriva dalla query string non entra mai nella query: serve solo a scegliere una voce della lista.
Il WAF non risolve il problema?
Riduce gli scanner automatici e il rumore nei log, ma non conosce la tua applicazione: una query costruita male resta sfruttabile anche dietro un WAF. Va considerato un livello in più, non la difesa principale.
Che permessi deve avere l'utente database dell'applicazione?
Solo quelli che le servono: SELECT, INSERT e UPDATE sulle tabelle che usa, nessun DDL e nessun accesso al catalogo di sistema. Le migrazioni girano con un secondo account, usato solo dal deploy.