ELAI S.r.l.

Cuando un documento intenta mandar al agente: separar datos, instrucciones y permisos

Un experimento local explica el control de herramientas de agentes de IA: procedencia, destinatarios, flujos y límites de las garantías.

Cuando un documento intenta mandar al agente: separar datos, instrucciones y permisos

Leer un documento no equivale a autorizarlo

Imaginemos un agente de IA encargado de leer una factura y añadir una nota para administración. El documento incluye una frase que pide cambiar el destino o borrar un registro. Una persona suele distinguir la factura como material de consulta, no como un superior que asigna tareas. Un sistema que mezcla instrucciones y textos recuperados necesita convertir esa distinción en una propiedad de su arquitectura.

La pregunta central es si podemos limitar las consecuencias de un documento hostil aunque el modelo lo interprete mal. Construimos un controlador que verifica tres condiciones antes de una escritura simulada: operación permitida, destino autorizado y receptor habilitado para leer todos los datos usados. Demostramos una propiedad de propagación de permisos y probamos ocho casos. No consultamos modelos ni enviamos mensajes: evaluamos un contrato de software, no la resistencia de un LLM a ataques.

La frontera entre propuesta y acción

Aquí un agente combina un modelo lingüístico con herramientas como búsqueda y actualización de registros. La inyección de instrucciones intenta convertir contenido no autorizado en órdenes. El texto puede ser gramatical y venir de una fuente que podemos leer legítimamente: el problema es la autoridad que recibe. Poder consultar un documento y permitirle decidir acciones son autorizaciones distintas.

Llamamos propuesta a la llamada candidata del modelo: herramienta y argumentos. Ejecución es el momento en que produce un efecto. El controlador se sitúa entre ambos. No juzga si una frase parece sospechosa: verifica propiedades del encargo y los permisos. El único efecto autorizado es añadir una nota interna a finance, identificador simbólico del departamento, con información que este pueda leer.

Fijamos las hipótesis: encargo inicial, controlador y metadatos son fiables; contenido y resultados lingüísticos no. Toda escritura pasa por el controlador. El atacante no modifica Python ni fabrica etiquetas de autorización. Son supuestos del modelo didáctico, no garantías de la dataclass. Si el modelo tiene además una shell libre, este ejemplo no constituye una sandbox que la vuelva segura.

Cada valor lleva dos datos adicionales

El texto solo no dice quién puede leerlo ni de dónde viene. Asociamos a cada valor v lectores permitidos R(v) y fuentes S(v). La factura a admite finance y manager; el presupuesto reservado b, solo manager. Son identidades resueltas por el sistema, no palabras que el documento inventa para obtener acceso. El código usa frozenset, cuyos elementos no se modifican directamente.

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

∩ indica intersección: conservamos solo lectores autorizados por ambos datos. Si un resumen contiene factura y presupuesto, finance no puede leerlo porque no tenía acceso al segundo. La unión sería incorrecta: ampliaría acceso al presupuesto a quien solo podía leer la factura. Combinar valores no debe convertirse en un atajo para ampliar permisos.

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

Para fuentes usamos unión, ⋃: el resultado conserva todos los orígenes de los que depende. f representa una transformación que declara correctamente cada entrada, como concatenar textos o resumirlos. El controlador usa R para decidir acceso y registra S para explicar la decisión. Conocer la procedencia no convierte el contenido en verdadero: una fuente identificada también puede contener errores.

La intersección es conservadora. Si un resumen lee un documento reservado pero repite solo una frase pública, mantenemos la restricción: no se ha demostrado que el secreto no influyera en elegirla. Relajar la etiqueta requiere otra regla, desclasificación, establecida por una autoridad distinta del texto de entrada. No la implementamos: las transformaciones no amplían R.

Tres condiciones antes de escribir

La llamada candidata contiene operación o, destino d y cuerpo b. El primer control solo admite append_internal_note. El segundo exige finance elegido por el encargo fiable, no extraído del documento. El tercero exige que finance pertenezca a R(b). Todos deben pasar: un destino aprobado no autoriza cualquier contenido, y contenido legible no autoriza cualquier operación.

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

∧ significa «y»: basta una condición falsa para bloquear. trusted_destination no es un clasificador lingüístico, sino un atributo asignado por el contexto fiable. En C4, finance procede del documento y se rechaza aunque coincida con el nombre esperado. La política es deliberadamente restrictiva: conservar el destino original evita aceptar propuestas del material consultado.

El control debe preceder al efecto. Solo si allow es verdadero añadimos a mock_log, lista en memoria que simula la escritura. No llamamos servicios remotos ni prometemos transacciones distribuidas. En producción habría que impedir rutas alternativas y evitar cambios de objeto, destino o permisos entre control y ejecución. Una comprobación correcta sobre datos obsoletos no protege la acción real.

Una demostración: los permisos no se amplían

Podemos afirmar más que «funcionan ocho casos», pero solo bajo las hipótesis dadas. Consideremos transformaciones que siempre usan intersección y registran todas sus entradas. Cada lector permitido del valor final v debe estar autorizado por todas sus fuentes. Es cierto para los valores iniciales porque sus metadatos se suponen correctos. Veamos por qué cada transformación lo conserva.

Si c depende de a y b, una identidad r en R(c) pertenece por definición a R(a) y R(b). Si la propiedad vale para ambos, r puede leer todas sus fuentes respectivas y, por tanto, su unión: las fuentes de c. Repetir el paso da una prueba inductiva. La comprobación r ∈ R(c) impide entregar explícitamente c a un lector excluido por alguna fuente.

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

La conclusión cubre el flujo explícito de valores etiquetados. No prueba ausencia de vulnerabilidades, autenticidad de metadatos ni cobertura de todo canal informativo. Depende de las hipótesis: olvidar una entrada, saltarse el control o etiquetar mal el origen queda fuera de la prueba. Su utilidad es señalar dónde concentrar la revisión.

Ocho llamadas candidatas y sus resultados

Construimos directamente las llamadas, sin pedirlas a un modelo. C1 añade el resumen de factura a finance y pasa. C2 intenta otra operación y se bloquea. C3 propone un destino externo; C4 propone finance extraído del documento: ambos incumplen la regla de destino. No medimos cuántos intentos inventaría un atacante ni la probabilidad de que el modelo los siguiera.

C5 inserta directamente el presupuesto reservado; C6 lo combina con el resumen permitido; C7 escribe su importe con palabras. Los tres fallan el control de lectores: cambiar la forma no cambia permisos. C8 inventa 999 euros en lugar de 120 y pasa: operación, destino y lectura están autorizados. Ese caso impide presentar la política como verificador de verdad.

CasoOperaciónDestinoLectoresPermitido
C11111
C20110
C31000
C41010
C51100
C61100
C71100
C81111

En la tabla, 1 indica control superado y 0 fallido; la última columna es la conjunción. El registro simulado contiene C1 y C8, mientras bloquea seis llamadas. «Seis de ocho» no es una tasa de defensa: elegimos casos manualmente, no una muestra representativa de ataques. El resultado reproducible es que el código aplica la política declarada, incluida su incapacidad para detectar el importe inventado.

Matriz calculada por el script. Cada fila es una llamada sintética, no un ataque observado en un modelo. C8 pasa como C1 pese al importe falso: autorización y exactitud del contenido son propiedades distintas.
Matriz calculada por el script. Cada fila es una llamada sintética, no un ataque observado en un modelo. C8 pasa como C1 pese al importe falso: autorización y exactitud del contenido son propiedades distintas.

El punto frágil: perder etiquetas al transformar

Añadimos un contraejemplo aparte: un componente toma el presupuesto reformulado, extrae la cadena y crea un objeto con lectores de la factura. Ahora el controlador permite pasar. No ha descubierto que el texto sea inocuo: se perdió la información necesaria. Incluimos deliberadamente ese componente defectuoso y comprobamos que difiera de la propagación correcta.

Por tanto, serialización, cachés, pasos entre servicios y errores forman parte de la frontera protegida. Un control final no basta si antes reconstruimos todo desde cadenas sin procedencia. Tampoco basta un objeto Python «inmutable»: el código arbitrario puede crear otro. Un sistema real debe asignar y conservar metadatos mediante componentes fiables, separados de la capacidad del modelo para proponer texto.

Lo que este experimento no garantiza

También hay dependencias implícitas. Si la aparición de una nota depende de información reservada, el observador aprende algo aunque el cuerpo sea una constante pública. Rastrear textos concatenados no basta. No interpretamos condicionales arbitrarios ni controlamos tiempos o errores, ni demostramos ausencia de esos canales. La propiedad probada se limita a flujos explícitos declarados.

Tampoco verificamos un plan generado por un modelo. Si se interpreta mal el encargo y la política concede demasiado, el controlador aprobará una acción indeseada. Definir permisos es parte del problema, no un trámite posterior. Nuestro caso es simple por elegir una operación y destino fijo; procesos abiertos necesitan más objetos, roles y condiciones explícitas.

Un filtro lingüístico y un controlador responden preguntas distintas. Uno reconoce contenido problemático; el otro verifica una propuesta concreta frente a un contrato. Pueden combinarse, pero no atribuimos eficacia no medida al filtro. Bloquear toda escritura impediría también C1. Comparar arquitecturas requiere tanto violaciones como trabajo útil completado, con costes y contextos documentados.

Conexión con la investigación y verificación

CaMeL, preprint de Debenedetti y colegas, v2 del 24 de junio de 2025, separa planificación y lectura no fiable y aplica políticas de flujo. AgentDojo distingue utilidad y seguridad; los autores discuten límites y canales laterales. Nuestro controlador no lo reproduce.

El código adjunto es local y usa la biblioteca estándar de Python; Matplotlib solo genera la figura. derived conserva intersección de lectores y unión de fuentes; policy devuelve tres booleanos y su conjunción; run ejecuta casos y comprueba resultados con aserciones. El registro muestra efectos solo en C1 y C8. El contraejemplo defectuoso queda separado y declarado fuera de las hipótesis.

No damos latencia del agente, coste en tokens ni porcentaje de ataques detenidos: no los medimos. Intersecar conjuntos pequeños tiene un coste distinto de consultar modelos; un sistema completo añade identidad, recuperación de permisos, propagación entre servicios y registro. Para EL-AI es análisis técnico asistido por IA, no anuncio de una función disponible ni de una auditoría completada.

La respuesta: leer no concede acciones ilimitadas

Un documento puede proponer instrucciones, pero eso no debe conceder operaciones ni destinos nuevos. En nuestro modelo restringido, tarea fija, permisos que acompañan a los datos y control previo al efecto bloquean clases concretas de llamadas no autorizadas. La garantía termina al perder procedencia o dejar canales sin control. Es distinta de la corrección del resumen: el agente debe respetar permisos y aportar información fundamentada. C8 muestra por qué ninguna propiedad sustituye a la otra.

Bibliografía

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'])

Código, datos e instrucciones · JSON. Cálculos didácticos ejecutados con Python 3.14.0; figuras con Matplotlib 3.11.2. Análisis asistido por IA, sin afirmar revisión por pares ni humana. Portada original ImageGen ilustrativa: no documenta personas, sedes ni instalaciones de EL-AI. Fuentes consultadas el 4 de octubre de 2026.