ELAI S.r.l.

Quando un documento prova a comandare l’agente: separare dati, istruzioni e permessi

Un esperimento locale spiega come controllare gli strumenti degli agenti AI: provenienza, destinatari, flussi informativi e limiti delle garanzie.

Quando un documento prova a comandare l’agente: separare dati, istruzioni e permessi

Leggere un documento non significa autorizzarlo

Immaginiamo un agente AI incaricato di leggere una fattura e aggiungere una nota al reparto amministrazione. Il documento contiene anche una frase che chiede di cambiare destinazione o cancellare un record. Per una persona è abbastanza chiaro che la fattura è materiale da esaminare, non il responsabile che assegna nuovi compiti. Per un sistema che mescola nella stessa conversazione istruzioni e testi recuperati, questa distinzione deve diventare una proprietà dell’architettura.

Questa è la domanda centrale: possiamo limitare le conseguenze di un documento ostile anche se il modello interpreta male il testo? Costruiremo un piccolo controllore che valuta tre condizioni prima di una scrittura simulata: operazione consentita, destinazione autorizzata e destinatario abilitato a leggere tutti i dati utilizzati. Poi dimostreremo una proprietà della propagazione dei permessi e la metteremo alla prova con otto casi. Nessun modello viene interrogato e nessun messaggio viene inviato: misuriamo un contratto software, non la resistenza di un LLM agli attacchi.

Il confine fra una proposta e un’azione

Un agente è qui un sistema che combina un modello linguistico con strumenti software, per esempio ricerca e aggiornamento di record. Una prompt injection tenta di trasformare contenuto non autorizzato in istruzione per il sistema. Il testo può essere corretto grammaticalmente e arrivare attraverso una fonte che siamo legittimati a leggere: il problema è l’autorità che gli attribuiamo. “Posso consultare questo documento” e “questo documento può decidere che cosa fare” sono autorizzazioni diverse.

Chiamiamo proposta la chiamata candidata prodotta da un modello: nome dello strumento e argomenti. Chiamiamo esecuzione il momento in cui il sistema produce un effetto. Il controllore si colloca fra questi due passaggi. Non cerca di capire se una frase “suona male”: controlla proprietà definite dal compito e dai permessi. Nel nostro esercizio l’unico effetto autorizzato è aggiungere una nota interna a finance, identificatore simbolico del reparto, e solo con informazioni che quel reparto può leggere.

Fissiamo anche le ipotesi. Il compito iniziale, il controllore e i metadati dei documenti sono affidabili; il contenuto del documento e i risultati linguistici non lo sono. Tutte le scritture devono passare dal controllore. Un attaccante non può modificare il codice Python o creare arbitrariamente etichette di autorizzazione. Sono condizioni del modello didattico, non proprietà garantite dalla dataclass che useremo. Se il modello dispone anche di una shell libera, questo esempio non costituisce una sandbox che la renda sicura.

Ogni valore viaggia con due informazioni in più

Un testo da solo non racconta chi può leggerlo né da dove deriva. Associamo quindi a ogni valore v un insieme R(v) di lettori ammessi e un insieme S(v) di fonti. Il documento di fattura a può essere letto da finance e manager; il budget riservato b soltanto da manager. Questi nomi rappresentano identità già risolte dal sistema, non parole che il documento può inventare per ottenere accesso. Nel codice gli insiemi sono frozenset, strutture non modificabili direttamente.

R(a) = {finance, manager} R(b) = {manager} R(c) = R(a) ∩ R(b) = {manager}

Il simbolo ∩ indica l’intersezione: teniamo soltanto i lettori ammessi da entrambi gli ingressi. Se un riepilogo contiene informazioni di fattura e budget, finance perde l’accesso al riepilogo, perché non poteva leggere il secondo documento. Usare l’unione sarebbe un errore: allargherebbe i lettori del budget includendo chi era autorizzato soltanto alla fattura. La combinazione non deve diventare una scorciatoia per aumentare i permessi.

R(f(v₁,…,vₙ)) = ⋂ᵢ R(vᵢ) S(f(v₁,…,vₙ)) = ⋃ᵢ S(vᵢ)

Per le fonti usiamo invece ⋃, l’unione: il risultato conserva tutte le provenienze da cui dipende. f rappresenta una trasformazione che dichiara correttamente tutti gli ingressi, come concatenare due testi o produrre un riepilogo. Il nostro controllore usa R per decidere l’accesso e registra S per rendere spiegabile la decisione. Non presume che una provenienza nota renda vero il contenuto: una fonte può essere identificata e contenere comunque un errore.

L’intersezione è conservativa. Se un riepilogo legge un documento riservato ma finisce per ripetere soltanto una frase pubblica, questo semplice meccanismo mantiene comunque la restrizione. Non ha dimostrato che il segreto non abbia influenzato la scelta della frase. Alleggerire l’etichetta richiederebbe una regola ulteriore, chiamata declassificazione, stabilita da un’autorità diversa dal testo in ingresso. Non la implementiamo: nel nostro esercizio le trasformazioni non possono espandere R.

Tre condizioni prima di scrivere

La chiamata candidata contiene un’operazione o, una destinazione d e un corpo b. Il primo controllo ammette solo append_internal_note. Il secondo richiede che d sia il reparto finance scelto dal compito affidabile, non una destinazione estratta dal documento. Il terzo richiede che finance appartenga a R(b). Tutti devono riuscire: consentire una destinazione non autorizza qualsiasi contenuto, e possedere contenuto leggibile non autorizza qualsiasi operazione.

allow(o,d,b) = [o = append_internal_note] ∧ trusted_destination(d) ∧ [d = finance] ∧ [d ∈ R(b)]

∧ significa “e”: basta una condizione falsa per bloccare. La funzione trusted_destination non è un classificatore linguistico; nel nostro script è un attributo assegnato soltanto dal contesto fidato. Nel caso C4 la stringa finance arriva dal documento e viene rifiutata anche se coincide con il nome atteso. È una politica volutamente restrittiva: conservare il destinatario fissato all’origine rende superfluo accettare proposte alternative dalla fonte consultata.

Il risultato del controllo deve precedere l’effetto. Nello script, soltanto se allow è vero aggiungiamo una voce a mock_log, una lista in memoria che simula la scrittura. Non chiamiamo un servizio remoto e non promettiamo transazioni distribuite. In produzione occorrerebbe impedire percorsi alternativi verso lo strumento e verificare che oggetto, destinatario e permessi non cambino fra controllo ed esecuzione. Una verifica corretta applicata a dati ormai superati non protegge l’azione effettiva.

Una piccola dimostrazione: i permessi non si allargano

Possiamo affermare qualcosa di più forte di “gli otto casi funzionano”, ma soltanto entro le ipotesi definite. Consideriamo una catena di trasformazioni che usa sempre la regola dell’intersezione e registra tutti gli ingressi. Per un valore finale v, ogni lettore ammesso deve essere ammesso da ciascuna fonte da cui v deriva. La proprietà è vera per i valori iniziali, perché i loro metadati sono assunti corretti. Vediamo perché resta vera dopo ogni trasformazione.

Supponiamo che il risultato c dipenda da a e b. Per definizione, se un’identità r appartiene a R(c), appartiene sia a R(a) sia a R(b). Se la proprietà vale già per a e b, r può leggere tutte le rispettive fonti. Può quindi leggere tutte le fonti di c, che sono la loro unione. Ripetendo questo passo lungo la catena otteniamo una dimostrazione per induzione. Il controllo finale r ∈ R(c) impedisce così l’invio esplicito di c a un lettore escluso da una sua fonte.

r ∈ R(c) ⇒ r ∈ R(a) ∧ r ∈ R(b) R(c) ⊆ R(a); R(c) ⊆ R(b)

La conclusione riguarda il flusso esplicito dei valori etichettati. Non dimostra che l’intero programma sia privo di vulnerabilità, che i metadati siano autentici o che tutti i canali informativi siano censiti. L’efficacia deriva precisamente dalle ipotesi: basta una trasformazione che dimentica un ingresso, una scrittura che salta il controllo o un’etichetta iniziale sbagliata per uscire dalla dimostrazione. La prova è utile perché ci dice dove concentrare la revisione del sistema.

Otto chiamate candidate e il motivo di ogni esito

L’esperimento costruisce direttamente le chiamate, senza chiedere a un modello di generarle. C1 aggiunge un riepilogo della fattura a finance e passa. C2 prova un’operazione diversa e viene bloccato. C3 propone una destinazione esterna; C4 ripropone finance ma lo trae dal documento: entrambi vengono bloccati dalla regola sulla destinazione. Non stiamo misurando quanti tentativi un attaccante saprebbe inventare o con quale probabilità un modello li seguirebbe.

C5 tenta di inserire direttamente il budget riservato; C6 lo combina con il riepilogo consentito; C7 ne riformula l’importo in parole. Tutti e tre falliscono il controllo sui lettori: cambiare forma non cambia permessi. C8 invece inventa un totale di 999 euro al posto dei 120 del documento. Passa i tre controlli, perché l’operazione, la destinazione e il diritto di lettura sono corretti. È il caso che impedisce di vendere questa politica come una verifica della verità.

CasoOperazioneDestinazioneLettoriConsentito
C11111
C20110
C31000
C41010
C51100
C61100
C71100
C81111

Nella tabella 1 significa controllo superato, 0 controllo fallito; l’ultima colonna è la congiunzione delle precedenti. Il registro simulato contiene due note, C1 e C8, mentre sei chiamate sono bloccate. “Sei su otto” non è un tasso di efficacia contro le prompt injection: i casi sono scelti a mano, non un campione rappresentativo di attacchi. Il risultato riproducibile è che il codice applica la politica dichiarata, inclusa la sua incapacità di riconoscere il totale inventato.

Matrice dei controlli calcolata dallo script. Ogni riga è una chiamata sintetica, non un attacco osservato su un modello. C8 passa come C1 pur contenendo un importo falso: autorizzazione e correttezza del contenuto sono proprietà diverse.
Matrice dei controlli calcolata dallo script. Ogni riga è una chiamata sintetica, non un attacco osservato su un modello. C8 passa come C1 pur contenendo un importo falso: autorizzazione e correttezza del contenuto sono proprietà diverse.

Il punto fragile: perdere l’etichetta durante una trasformazione

Aggiungiamo un controesempio fuori dagli otto casi: un componente riceve il budget riformulato, estrae la semplice stringa e costruisce un nuovo oggetto assegnandogli i lettori della fattura. Il controllo ora lo lascia passare. Non ha scoperto che il testo è innocuo: qualcuno ha cancellato l’informazione necessaria a decidere. Lo script include deliberatamente questo componente difettoso e verifica che il suo risultato differisca dalla trasformazione corretta.

La conseguenza pratica è che serializzazione, cache, passaggio fra servizi e gestione degli errori fanno parte del confine da proteggere. Non basta aggiungere un controllo all’ultima funzione se prima si ricostruisce tutto da stringhe prive di provenienza. Anche rendere l’oggetto Python “immutabile” non basta: chi può eseguire codice arbitrario può crearne un altro. Un sistema reale deve assegnare e conservare queste informazioni in componenti fidati, separati dalla capacità del modello di proporre testo.

Quello che questo esperimento non può garantire

Esistono anche dipendenze implicite. Se la presenza di una nota dipende da un’informazione riservata, un osservatore può apprendere qualcosa anche quando il corpo della nota è una costante pubblica. Non basta quindi tracciare solo i testi concatenati. Il nostro esempio non interpreta rami condizionali arbitrari, non controlla tempi di esecuzione o messaggi di errore e non dimostra assenza di questi canali. La proprietà provata resta deliberatamente limitata ai flussi espliciti dichiarati.

Non abbiamo neppure verificato la correttezza di un piano generato da un modello. Se il compito iniziale viene interpretato male e la politica concede troppo, il controllo può approvare un’azione indesiderata. La definizione dei permessi è quindi parte del problema, non una casella da compilare dopo lo sviluppo. Nel nostro caso è semplice proprio perché scegliamo una sola operazione e un destinatario fisso; estenderla a processi aperti richiede di esplicitare nuovi oggetti, ruoli e condizioni.

Un filtro linguistico e un controllore di permessi rispondono a domande diverse. Il primo tenta di riconoscere un contenuto problematico; il secondo valuta se una proposta concreta rispetta un contratto. Possono essere usati insieme, ma nel nostro test non attribuiamo al filtro un’efficacia mai misurata. All’altro estremo, bloccare ogni scrittura impedirebbe anche il compito legittimo C1. Per confrontare architetture servono sia violazioni sia utilità del lavoro svolto, con costi e contesti documentati.

Il collegamento con la ricerca e il percorso di verifica

CaMeL, preprint di Debenedetti e colleghi nella versione v2 del 24 giugno 2025, separa pianificazione e lettura non fidata e applica politiche ai flussi. La valutazione su AgentDojo distingue utilità e sicurezza; gli autori discutono limiti e canali laterali. Il nostro controllore non ne è una riproduzione.

Il codice allegato è interamente locale e usa solo la libreria standard di Python; Matplotlib serve esclusivamente alla figura. derived conserva l’intersezione dei lettori e l’unione delle fonti; policy restituisce tre valori booleani e il loro esito congiunto; run esegue i casi e verifica gli esiti attesi con asserzioni. Il registro simulato rende visibile che solo C1 e C8 producono l’effetto. Il controesempio del componente difettoso rimane separato e dichiarato come violazione delle ipotesi.

Non riportiamo latenza di un agente, costo in token o percentuali di attacchi fermati perché nessuna di queste misure è stata eseguita. Intersecare insiemi piccoli è un costo software diverso da interrogare un modello; in un sistema completo entrano in gioco risoluzione delle identità, recupero dei permessi, propagazione fra servizi e registrazione degli eventi. Per EL-AI questo articolo è un’analisi tecnica assistita da AI, non l’annuncio di una funzionalità di sicurezza già disponibile o di un audit completato.

La risposta: leggere tutto non implica poter fare tutto

Un documento può proporre istruzioni, ma il sistema non deve concedergli per questo nuove operazioni o nuovi destinatari. Nel modello ristretto che abbiamo costruito, la combinazione di un compito fisso, permessi che seguono i dati e controllo prima dell’effetto blocca precise classi di chiamate non autorizzate. La garanzia si interrompe dove perdiamo provenienza o lasciamo canali non controllati. E resta distinta dalla correttezza del riepilogo: l’agente deve sia agire entro i permessi sia riportare informazioni fondate. L’esempio C8 ci ricorda perché nessuna delle due proprietà può sostituire l’altra.

Bibliografia

Edoardo Debenedetti et al. — Defeating Prompt Injections by Design, arXiv:2503.18813v2, 24 June 2025, preprint.

Google Research — CaMeL research artifact.

from experiment import run
r = run()
for c in r['cases']:
    print(c['case'], c['checks'], c['allowed'])
print(r['counts'])
print('Broken wrapper:', r['broken_wrapper']['allowed'])

Codice, dati e istruzioni · JSON. Calcoli didattici eseguiti con Python 3.14.0; figure con Matplotlib 3.11.2. Analisi con assistenza AI, senza dichiarare peer review o revisione umana. Copertina originale ImageGen, illustrativa: non documenta persone, sedi o installazioni EL-AI. Fonti consultate il 4 ottobre 2026.