Torna agli scritti

Il prompt non è un confine di sicurezza

Quando è un LLM a scrivere la query, autorizzare chi chiede non basta. Conta ciò che la risposta riesce a toccare.

Nei sistemi che generano SQL, filtri o codice con un modello linguistico, ci sono due domande di autorizzazione, ed è facile rispondere solo alla prima:

  • Chi può richiedere la query?
  • Che cosa può toccare la query generata?

Autorizzare la prima e ignorare la seconda è chiudere la porta e lasciare aperta la finestra. L'utente aveva il permesso di fare domande sui dati della sua unità; l'SQL che il modello ha scritto non lo sa.

Carica solo ciò che è già stato autorizzato

La protezione più forte contro un modello che accede a ciò che non deve è che il dato non sia lì. Invece di aprire un'engine con tutti i contesti e fidarsi che il modello scelga quello giusto, l'esecuzione riceve solo ciò che è già passato per l'autorizzazione:

ctx = authorize_data_context(user, context_id)  # predicato lato server
engine = load_engine_only(ctx.storage_prefix)

Se il processo di quell'esecuzione non vede gli altri dataset, nessun prompt, per quanto creativo, fa arrivare il modello fin lì.

Il system prompt è un suggerimento

“Usa solo queste tabelle” nel system prompt non è un controllo. Il modello è un componente probabilistico e prima o poi farà qualcos'altro, per errore o perché qualcuno gliel'ha chiesto con le buone. L'output deve passare per una validazione deterministica prima di essere eseguito:

domanda
  -> contesto autorizzato
  -> il modello genera un candidato
  -> parser / AST / validazione
  -> rifiuto se non valido
  -> audit
  -> esecuzione su engine con scope
  -> risposta

Il guard minimo

Per l'SQL generato, il minimo che considero accettabile:

  • un solo statement;
  • sola lettura, con DDL, DML e comandi amministrativi bloccati;
  • nessuna funzione di file o di rete;
  • ogni relazione referenziata dentro l'allowlist del contesto;
  • limite di righe, timeout e budget di memoria;
  • connessione read-only, anche se il guard fallisce.

L'ultimo punto è quello che dà tranquillità: il guard è uno strato, la connessione read-only è un altro e l'engine con scope è un terzo. Nessuno deve essere perfetto da solo.

La regex è un'impalcatura

Si può cominciare con una regex, purché coperta da test avversariali. Ma la regex è una base provvisoria, non una destinazione. Commenti, stringhe, identificatori tra virgolette e sintassi specifica del dialetto rendono fragile qualsiasi regex:

SELECT * FROM "public"."Pazienti" /* ; DROP */
SELECT * FROM report WHERE note = 'FROM secrets'

Un parser compatibile con il dialetto che eseguirà la query trasforma la domanda “sembra pericoloso?” in “quali relazioni referenzia quest'albero?”, e questa seconda domanda ha una risposta esatta.

L'injection arriva dai dati

La domanda dell'utente non è l'unico input non fidato. Nome di colonna, valore di cella, metadati di un foglio di calcolo, testo di un PDF ingerito: tutto questo arriva nel contesto del modello e può contenere qualcosa che sembra un'istruzione. Delimitare questi elementi come dati aiuta, ma la difesa decisiva resta validare l'output. Se la risposta può essere solo un SELECT su relazioni autorizzate, un'istruzione nascosta in un foglio di calcolo non ha dove andare.

Meno strumenti, meno superficie

Nel percorso delle query sensibili, l'agente riceve solo lo stretto necessario. Se il compito è lettura analitica, niente scrittura su filesystem, rete arbitraria, shell, Python libero, mutazioni del database o credenziali di altri servizi. Il privilegio minimo vale per gli agenti come vale per le persone, e a maggior ragione: l'agente legge tutto ciò che gli passa davanti.

Questo si collega a Maschera i dati prima di chiamare l'LLM: uno strato si occupa di ciò che il modello vede, l'altro di ciò che riesce a fare.