PRODPiattaforma BaaS europea sovranaApri Dashboard →

IA nativa · 9 lettura minima

Proteggi NL2SQL contro l'iniezione SQL LLM

Affane Daylami · Fondateur · 16 aprile 2026

Torniamo al blog

Un endpoint NL2SQL trasforma una domanda in linguaggio naturale in una query SQL, quindi questa query viene eseguita sul tuo database. Il rischio quindi non è la classica SQL injection, una stringa mal scappata in un form: è un modello di linguaggio che da solo decide quale SQL scrivere. Chiedere educatamente al modello di generare solo SELECT nel prompt del sistema non impedisce nulla a livello strutturale, è un'istruzione, non un controllo di accesso. L'unico metodo che funziona è validare l'SQL generato successivamente, con un parser che ne costruisce l'albero sintattico.

Questo testo inglese è stato generato automaticamente dall'originale francese e non è stato ancora rivisto.
Questa pagina è stata tradotta automaticamente. Fa fede la versione inglese.

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, LIMIT obbligatorio 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 in FILTER, in un intra-aggregato ORDER BYo in OFFSET.
#
Il vero problema

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.

L'immagine che aiuta

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.

#
Passaggio 1

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.

nl2sql/validator.rsrust
let dialect = PostgreSqlDialect {};
let mut statements = Parser::parse_sql(&dialect, sql)
    .map_err(|e| AuraError::BadRequest(format!("Erreur de parsing SQL: {}", e)))?;

if statements.len() > 1 {
    return Err(AuraError::BadRequest("Multi-statements non autorisés".to_string()));
}

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.

#
Passaggio 2

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 respintaPerché è pericoloso?
CTE/CONPuò concatenare logica aggiuntiva non intenzionale prima della SELECT finale.
Sottoquery, UNION / INTERSECT / EXCEPTEspande la superficie di ciò che una singola domanda può fare in una singola query.
SELEZIONA...INCrea una tabella: la scrittura travestita da lettura.
PER AGGIORNAMENTO / PER CONDIVIDEREInstallazione 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.

#
Passaggio 3

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.

nl2sql/tests.rsrust
#[test]
fn test_rejette_fonction_interdite_dans_filter() {
    assert!(validate(
        "SELECT count(*) FILTER (WHERE pg_sleep(10) IS NOT NULL) FROM users",
        None, None
    ).is_err());
}

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.

#
Passaggio 4

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.

réponse /v1/ai/{project_id}/nl2sql (extrait)json
{
  "sql": "SELECT count(*) FROM orders WHERE status = 'paid' LIMIT 100",
  "limit": 100,
  "limit_injected": true
}
Informazioni

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.

#
Passaggio 5

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.

#
Passaggio 6

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.

#
Lista di controllo

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.
#
Difesa in profondità

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.

#
Domande frequenti

Domande frequenti

Il prompt engineering è completamente inutile per proteggere NL2SQL?+
No, rimane utile per indirizzare il modello verso un SQL corretto e pertinente la maggior parte delle volte. Ma non è un controllo di sicurezza: una domanda ambigua o manipolata può aggirarlo, e un’istruzione tempestiva non blocca fisicamente nulla. Un validatore strutturale dopo la generazione rimane necessario, indipendentemente dalla qualità del prompt.
Dovremmo comunque attivare RLS se l'SQL generato è già convalidato e limitato a SELECT?+
Sì. Il validatore limita la forma della richiesta (nessuna sottoquery, nessuna funzione non whitelist, LIMIT limitato), non i diritti commerciali sui dati. La RLS rimane il livello che decide quali linee un utente specifico ha il diritto di vedere, anche all'interno di una SELECT perfettamente valida.
Una lista bianca chiusa di dieci funzioni non limita troppo le possibili domande?+
Sì, ed è volontario. Copre la maggior parte delle analisi di lettura (conteggio, somma, media, minimo, massimo, inferiore, superiore, coalesce, date_trunc, ora) e nega tutto il resto per impostazione predefinita, inclusa una funzione legittima che nessuno ha ancora pensato di aggiungere. Ogni aggiunta deve essere una decisione esplicita, non una svista inserita in una lista nera.

PRONTO PER L'IMPLEMENTAZIONE?

Il tuo backend in cinque minuti.

Nessuna carta di credito richiesta · 500 MB gratuiti · 50.000 MAU