Questa guida descrive in dettaglio il metodo che funziona davvero: restrizione strutturale a SELECT, whitelist chiusa di funzioni, limite di riga obbligatorio e blocco dello schema interrogato. Ogni passaggio si basa sul validatore effettivamente implementato nel motore NL2SQL di Aurabase, una funzionalità della sua intelligenza artificiale nativa integrata nel backend, non un servizio di terze parti messo insieme a posteriori. Se l'argomento è nuovo per te, la nostra panoramica di NL2SQL pone le basi e il tutorial passo passo mostra come costruire l'endpoint completo.
L'essenziale
- Il prompt engineering ("genera solo SELECT") è non un controllo di sicurezza: un modello può avere allucinazioni, essere guidato da una domanda ambigua o semplicemente ignorare le istruzioni.
- La validazione che vale è strutturale: un parser costruisce l'albero sintattico (AST) della richiesta e rifiuta per default tutto ciò che non è esplicitamente autorizzato.
- Quattro livelli concreti limitano il rischio: SELECT rigorosa (né sottoquery, né CTE, né UNION), whitelist chiusa di dieci funzioni,
LIMITobbligatorio e limitato, accesso bloccato al catalogo di sistema e schemi non tenant. - Lo schema interrogato deve provenire dal server , mai da un campo nella richiesta del client: altrimenti nulla impedisce a un chiamante di fornire il proprio schema per bypassare la convalida.
- In Aurabase, questo validatore (Rust crate
sqlparser) viene testato con casi contraddittori documentati nel codice: funzioni proibite nascoste inFILTER, in un intra-aggregatoORDER BYo inOFFSET.
Perché un'istruzione nel prompt del sistema non blocca nulla?
Un prompt di sistema che dice "genera solo query SELECT" è una preferenza, non una barriera. Il modello lo rispetta la maggior parte delle volte perché è stato addestrato a seguire le istruzioni, non perché un vincolo tecnico gli impedisca fisicamente di scrivere qualcos'altro. Due classi di fallimento rendono questa fiducia insufficiente nella produzione.
Il primo deriva dalla domanda stessa. Un utente, mal intenzionato o semplicemente creativo nella formulazione, può indirizzare la domanda in modo tale da spingere il modello verso SQL che non avrebbe dovuto scrivere: un join a una tabella sensibile, un filtro che elude la logica prevista, una chiamata di funzione di sistema. Il modello non distingue tra una domanda legittima e una mirata a manipolarla.
La seconda non richiede malizia. Un modello può avere allucinazioni sul nome di una tabella, dimenticare LIMIT richiesto dal prompt o generare un SELECT * senza alcuna restrizione su una tabella di grandi dimensioni. Il risultato è lo stesso in entrambi i casi: SQL potenzialmente costoso o invasivo, che ha superato il filtro del prompt e sta per essere eseguito su un database reale.
Il prompt del sistema rimane utile, indirizza il modello verso il risultato giusto la maggior parte delle volte. Ma un cartello di “divieto di accesso” non ferma chi non sa leggere o chi decide di ignorarlo. Hai bisogno di una porta chiusa dietro, non solo di un pannello davanti.
Analizza l'SQL generato in un albero di sintassi, mai in una stringa grezza
La prima linea di difesa consiste nell'analizzare l'SQL prodotto dal modello con un vero parser per il dialetto di destinazione, quindi convalidare la struttura risultante, non il testo grezzo. La ricerca di parole proibite nella stringa di caratteri (“DROP”, “DELETE”, “;”) viene banalmente aggirata: maiuscole e minuscole diverse, commento inserito al centro di una parola chiave, virgolette digitate. Un albero della sintassi descrive in modo inequivocabile cosa fa effettivamente la query.
Aurabase implementa questo passaggio con il crate Rust sqlparser e il suo dialetto PostgreSqlDialect. Anche prima dell'analisi, un primo filtro lessicale rifiuta due costruzioni difficili da ragionare correttamente una volta nell'albero: virgolette in dollari ($$...$$), che possono nascondere contenuto arbitrario in una stringa, e commenti su più righe (/* */), che possono nascondere la fine reale di un'istruzione.
Questo rifiuto di multi-istruzione da solo blocca la forma più nota di SQL injection mediante stacking: SELECT * FROM users; DROP TABLE users;--. Il parser restituisce solo un'istruzione utilizzabile, la seconda semplicemente non viene mai raggiunta, indipendentemente da come è formulata nella domanda originale.
Limitarsi strutturalmente a una semplice SELECT
Una volta ottenuto l'albero, la convalida più ampia consiste nell'accettare solo un tipo di nodo radice, una query (Statement::Query), e rifiutare tutto il resto: INSERT, UPDATE, DELETE, DROP, CREATE, ALTER. Non si tratta più di un'istruzione immediata, ma di una condizione sul tipo dell'oggetto analizzato, che nessuna abile formulazione della domanda può aggirare.
Anche all'interno di una SELECT, diversi costrutti rimangono pericolosi e meritano il loro esplicito rifiuto:
| Costruzione respinta | Perché è pericoloso? |
|---|---|
| CTE/CON | Può concatenare logica aggiuntiva non intenzionale prima della SELECT finale. |
| Sottoquery, UNION / INTERSECT / EXCEPT | Espande la superficie di ciò che una singola domanda può fare in una singola query. |
| SELEZIONA...IN | Crea una tabella: la scrittura travestita da lettura. |
| PER AGGIORNAMENTO / PER CONDIVIDERE | Installazione di serrature, rischio di contesa con il traffico produttivo. |
| Funzioni di tabella (generate_series, pg_read_file...) | Accesso al sistema o negazione del servizio tramite linee generate su richiesta. |
Un caso di test tratto dal repository illustra concretamente l'ultimo punto: SELECT * INTO backup FROM users viene rifiutato, anche se non contiene né parola chiave di scrittura visibile né funzione sospetta. Per escluderlo è sufficiente la forma della richiesta.
Whitelist delle funzioni, non blacklist
Una lista nera di funzioni proibite (pg_sleep, pg_read_file, dblink...) richiede di anticipare ciascuna funzione pericolosa una per una, mentre Postgres ne espone diverse centinaia. Una whitelist inverte l'onere della prova: sono autorizzate solo dieci funzioni, count, sum, avg, min, max, lower, upper, coalesce, date_trunc, now. Tutto il resto non è consentito per impostazione predefinita, inclusa una funzionalità legittima che nessuno ha ancora pensato di aggiungere.
Non sempre un solo passaggio di validazione è sufficiente. Un attraversamento strutturale dell'albero elenca i suoi punti di ingresso uno per uno (proiezione, WHERE, JOIN, GROUP BY...), ed è facile dimenticarne uno: una funzione proibita può essere nascosta in una clausola FILTER (WHERE pg_sleep(10) IS NOT NULL), in un intra-aggregato ORDER BY (sum(id ORDER BY pg_sleep(10))), in WITHIN GROUP, DISTINCT ONo OFFSET.
Il validatore Aurabase aggiunge quindi un secondo passaggio esaustivo, che attraversa tutte le espressioni dell'albero ovunque si trovino, indipendentemente dal percorso strutturale. È una difesa presunta in profondità: se il primo passaggio non riesce a cogliere un caso, il secondo lo raggiunge.
Delimitare le righe restituite: LIMIT obbligatorio e limitato
SELECT * rimane autorizzato, è utile per il data mining. Il rischio non è il protagonista, è l'assenza di un tetto su una query scritta da un modello: una domanda mal formulata può riportare indietro un'intera tabella, con il costo di memoria e il tempo di risposta che ciò implica.
Aurabase applica una regola semplice e trasparente. Se l'SQL generato non ha un LIMIT, il server ne aggiunge uno (100 righe per impostazione predefinita, valore annunciato al modello nel prompt del sistema). Se l'SQL richiede un LIMIT oltre il limite massimo (1000 righe per impostazione predefinita), la query viene rifiutata esplicitamente anziché ridotta automaticamente. Entrambi i valori sono configurabili sul lato server (AI_NL2SQL_DEFAULT_LIMIT, AI_NL2SQL_MAX_LIMIT), e il server si rifiuta addirittura di avviarsi se l'errore supera il limite.
Rifiutare piuttosto che tagliare silenziosamente ha un interesse diretto: un massimale applicato senza dirlo darebbe a chi chiama l'illusione che la sua richiesta sia stata onorata, mentre il risultato sarebbe stato troncato senza che lui se ne accorgesse. limit_injected dice sempre se il valore proviene dal modello o dal server.
Blocca l'accesso allo schema: catalogo di sistema e schema incrociato
Due distinte perdite minacciano un motore NL2SQL connesso a un database reale: l'accesso al catalogo del sistema Postgres e l'accesso a uno schema che non appartiene al chiamante. Entrambi si bloccano al momento della convalida, indipendentemente da qualsiasi policy RLS posta a valle.
pg_catalog fa sempre parte di search_path, il che significa che un nome non qualificato come pg_authid o pg_stat_activity accede direttamente, senza prefisso. Il validatore Aurabase blocca qualsiasi nome che inizia con pg_, così come information_schema e lo schema interno aura_console, qualificato o meno.
Su un nome a due componenti (schema.table), è consentito solo lo schema del progetto chiamante, qualsiasi altro valore viene rifiutato. Un nome con tre o più componenti viene automaticamente rifiutato. Questo limite a livello di query generata è in aggiunta all'isolamento a livello di database dettagliato nel nostro articolo suisolamento multi-tenant: uno impedisce all'SQL generato di prendere di mira un altro schema, l'altro impedisce alla connessione stessa di raggiungere un altro database. Nessuno dei due sostituisce l'altro.
Non lasciare mai che il client ridefinisca lo schema interrogato
Una trappola discreta è in agguato per qualsiasi API NL2SQL che accetta un parametro che descrive lo schema o le tabelle consentite nella query del client. Se questo stesso parametro viene utilizzato per costruire il prompt e convalidare l'SQL di output, un chiamante può mentire su ciò che è consentito e la convalida convalida quindi questa bugia anziché la realtà del database.
Aurabase analizza lo schema di base del progetto effettivo su ogni chiamata, con una breve cache di trenta secondi per le prestazioni, e rifiuta esplicitamente (errore 400) qualsiasi campo schema, allowed_schemao schema_context inviato nel corpo della richiesta, anziché accettarlo e quindi sovrascriverlo silenziosamente. La differenza conta: un campo accettato e poi ignorato dà l'illusione di un controllo che non esiste; un campo rifiutato lo dice subito.
Controlla la tua pipeline NL2SQL prima della produzione
Sia che utilizzi Aurabase o crei la tua pipeline su un LLM generico, i seguenti punti coprono ciò che più spesso viene trascurato.
Se scrivi tu stesso il validatore
- Analizza SQL con un vero parser per il tuo dialetto esatto, mai con la corrispondenza dei modelli in una stringa.
- Adotta un rifiuto predefinito: qualsiasi tipologia di nodo, qualsiasi funzione non esplicitamente autorizzata deve essere rifiutata, non solo i casi pericolosi già individuati.
- Accetta solo un'istruzione per query, questo è il rifiuto più semplice contro le query di stacking.
- Testare il validatore con casi contraddittori reali (funzione vietata in FILTER, in un ORDER BY intraaggregato, in OFFSET), non solo con casi evidenti.
- Nonostante tutto, esegui l'SQL validato con un ruolo Postgres con privilegi ridotti sullo schema previsto: il validatore limita la forma della query, il ruolo limita ciò che può fisicamente ottenere se un caso ti è sfuggito.
Se stai valutando un framework NL2SQL di terze parti
- Chiedere esplicitamente se la validazione è strutturale (AST) o solo un'istruzione tempestiva: la risposta cambia tutto.
- Verifica che un limite di linea venga applicato per impostazione predefinita e non solo documentato come best practice a tue spese.
- Verificare se lo schema utilizzato per la validazione può essere fornito dal client API, il che riaprirebbe esattamente la falla sopra descritta.
- Confronta diversi strumenti in base a questo criterio specifico prima di scegliere: il nostro confronto degli strumenti NL2SQL descrive in dettaglio ciò che distingue gli approcci disponibili nel 2026.
Il validatore riduce il rischio, non sostituisce RLS
Un valido validatore AST riduce il rischio alla fonte: l'SQL che raggiunge il tuo database ha già una forma nota e limitata. Tuttavia, non sostituisce i criteri RLS nelle tabelle sensibili, che decidono quali righe un determinato utente ha il diritto di vedere. I due livelli rispondono a domande diverse: il validatore limita la forma della query generata, la RLS limita i dati che può restituire per un utente specifico. Manteneteli entrambi attivi, anche se uno sembra ridondante rispetto all'altro.
NL2SQL copre domande strutturate sulle tue tabelle. Per domande su contenuti non strutturati, documenti, note, ticket, il RAG nativo di Aurabase segue una logica di sicurezza comparabile, dettagliata nel nostro tutorial sulla pipeline RAG su pgvector.