Seguridad de IA — Módulo 5: seguridad de agentes con herramientas
Quinto módulo del Curso de Seguridad de IA: el salto de riesgo cuando el modelo pasa de 'decir cosas' a 'hacer cosas'. Excessive agency, el problema del confused deputy, mínimo privilegio por herramienta, la intersección de permisos agente ∩ usuario, confirmación humana para acciones irreversibles y por qué la defensa de verdad vive en el diseño de las tools.
☰ Índice del contenido
- La tesis del módulo
- El salto de riesgo: de decir a hacer
- Excessive agency: los tres ejes
- El confused deputy: el ataque que hereda tus permisos
- Los principios de defensa
- Human-in-the-loop para lo irreversible
- Código: una tool con permisos, allowlist y confirmación
- La tabla del excessive agency
- Errores comunes
- La frase para llevarse
- Qué viene
- Material relacionado
En el módulo 4 recorrimos los jailbreaks y las técnicas de evasión: las formas de conseguir que el modelo diga lo que no debía decir. Hasta ahí, en el peor de los casos, el daño era ese — el modelo dice algo malo. Un consejo prohibido, una política inventada, un insulto, el system prompt filtrado. Grave, pero contenido en el texto. Este módulo cruza la línea que lo cambia todo: le conectamos herramientas al agente. Una tool es la capacidad de leer un correo, escribir en una base de datos, emitir un reembolso, mandar un mensaje, ejecutar un comando. Con tools el modelo deja de “decir cosas” y empieza a hacer cosas. Y un jailbreak que antes producía una frase molesta, ahora produce una acción con consecuencias en el mundo real, ejecutada con los permisos de tu sistema.
Un recordatorio, porque el material es defensivo: todo lo que viene sirve para endurecer tus propios agentes. Las técnicas de ataque aparecen únicamente para justificar el control que las neutraliza, igual que en los módulos anteriores.
La tesis del módulo
La seguridad de un agente no se decide en el prompt sino en el catálogo de tools. Cada tool es una acción que un ataque exitoso puede lograr: el conjunto de tools ES el conjunto de daños posibles. Por eso reducir el riesgo no es escribir un prompt más severo ni confiar en que el modelo se porte bien — es reducir lo que las tools permiten hacer. Un permiso que no otorgaste es una vulnerabilidad que no existe.
El salto de riesgo: de decir a hacer
Vale la pena hacerlo explícito, porque marca la diferencia entre los cuatro módulos anteriores y este. Un modelo sin tools es un generador de texto: por peligrosa que sea la frase que le arranques, sigue siendo una frase que alguien tiene que leer y actuar. Un modelo con tools es un actuador. La cadena “entrada maliciosa → salida del modelo → efecto en el mundo” ya no tiene un humano en el medio que pueda frenar.
Y el atacante lo sabe. El objetivo de un ataque a un agente casi nunca es que diga una grosería: es que use una tool que no debía usar, o que la use con argumentos que no debía. La injection indirecta que vimos en Evals es peligrosa precisamente por esto — el texto envenenado que el agente lee no busca cambiar el resumen, busca disparar una llamada a enviar_correo.
Excessive agency: los tres ejes
El OWASP Top 10 para LLM le puso nombre a esto en la categoría LLM06 — Excessive Agency (agencia excesiva: darle al agente más capacidad, permiso o autonomía de la que la tarea necesita, de modo que una salida inesperada, ambigua o manipulada del modelo termine en una acción dañina). No es un ataque: es una condición de tu diseño que convierte cualquier otro ataque en un incidente con efecto real. Vive en tres ejes independientes, y hay que auditar los tres.
- Exceso de funcionalidad. La tool hace más de lo que la tarea requiere. Diste
ejecutar_sqlcuando solo necesitabasbuscar_cliente_por_id. Conectaste un servidor MCP entero con veinte tools para usar dos. Cada capacidad de más es un daño de más que el atacante hereda gratis. - Exceso de permisos. La tool corre con credenciales más amplias de lo necesario. La tool de lectura usa una conexión con permiso de escritura “por comodidad”; la clave de la API tiene alcance de administrador cuando el agente solo consulta. El permiso sobrante no lo usa tu código legítimo — lo usa el ataque.
- Exceso de autonomía. El agente ejecuta acciones de alto impacto sin confirmación. Puede borrar, pagar, enviar o publicar sin que ningún humano apruebe. La autonomía es cómoda hasta que un input manipulado la aprovecha, y entonces el error se materializa a la velocidad de una llamada a función.
Ojo: los tres ejes son ortogonales. Puedes tener mínimo privilegio impecable en las credenciales (eje 2) y aun así regalar el sistema por exceso de funcionalidad (eje 1), porque expusiste una tool genérica y poderosa que no hacía falta. Audítalos por separado.
El confused deputy: el ataque que hereda tus permisos
Hay un patrón de ataque que explica por qué el mínimo privilegio no es opcional: el confused deputy (diputado confundido, un término clásico de seguridad de los años 80). La idea: un componente con privilegios legítimos es engañado por un tercero sin privilegios para que use ESOS privilegios en su nombre. El componente no está comprometido — hace exactamente lo que sabe hacer — pero lo hace a instancias de quien no debía.
En un agente LLM el diputado es el propio agente. Tiene tools con permisos reales (los tuyos, los del sistema). El tercero es el atacante que, vía injection — directa o indirecta —, le dice qué hacer. El agente ejecuta la acción con SUS permisos, no con los del atacante, que no tiene ninguno. Ahí está el filo: el atacante no necesita credenciales; le basta con controlar un texto que el agente vaya a leer.
EL CONFUSED DEPUTY EN UN AGENTE
atacante recursos
(sin permisos) protegidos
│ ▲
│ instrucción │ acción con
│ inyectada │ permisos del
▼ │ AGENTE
┌────────────┐ ┌───────┴──────┐
│ dato leído │ entra al contexto │ AGENTE │
│ (web, PDF, │ ─────────────────► │ (el diputado)│
│ correo) │ como si fuera │ tiene tools │
└────────────┘ una orden └───────┬──────┘
│
el agente NO está comprometido: │ usa su
hace lo que sabe hacer, pero a │ credencial,
instancias de quien no debía ▼ no la de él
┌────────────────┐
│ enviar_correo │
│ borrar_registro│
└────────────────┘
La lección del diagrama: el ataque no roba credenciales ni escala privilegios por la vía clásica. Usa los tuyos. Por eso la defensa no puede ser “autenticar mejor al atacante” — el atacante nunca se autentica. La defensa es recortar lo que el diputado puede hacer aunque esté confundido.
Los principios de defensa
Ninguno vive en el prompt. Todos viven en cómo defines y envuelves las tools.
Mínimo privilegio por tool. La regla base: la tool que no existe no se puede abusar. Antes de agregar una capacidad, pregunta si la tarea la necesita de verdad. Prefiere tools estrechas y específicas (marcar_ticket_resuelto) sobre tools anchas y genéricas (ejecutar_accion). Una tool estrecha limita el daño por construcción; una genérica delega la seguridad en el prompt, que es una sugerencia probabilística.
Separar lectura de escritura. Divide el catálogo en tools que solo consultan y tools que modifican. Las de lectura pueden ser liberales; las de escritura, escasas y custodiadas. Esta separación permite dar a un agente de soporte de nivel 1 solo el conjunto de lectura, y reservar la escritura para flujos con más control. Mezclar ambas en una sola tool “por comodidad” borra esa frontera.
Intersección de permisos: agente ∩ usuario. El principio más importante y el más olvidado. Un agente actúa en nombre de un usuario. Por lo tanto solo puede hacer lo que el agente Y ese usuario pueden hacer — la intersección, nunca la unión. Si el agente corre con una credencial de servicio potente pero el usuario que lo invoca es un cliente común, la acción debe limitarse a lo que ese cliente podría hacer por sí mismo. El agente jamás debe poder hacer más que el usuario en cuyo nombre actúa. Cuando esto se rompe, el agente se vuelve un canal de escalada de privilegios: el usuario le pide (o le inyecta) algo que él solo no podría, y el agente lo ejecuta con su credencial de servicio.
Scoping de credenciales por request. No le des a la tool una credencial fija y omnipotente. Emite credenciales acotadas al contexto de la petición: alcance mínimo, vigencia corta, atadas al usuario de turno. Así, aunque el agente sea confundido, la credencial que tiene en la mano ya está recortada al usuario y al momento.
Allowlist de destinos en la tool, no en el prompt. “Nunca mandes correos fuera de la empresa” en el system prompt es una sugerencia que un jailbreak rodea. Una allowlist (lista blanca: el conjunto cerrado de destinos permitidos) codificada dentro de la tool enviar_correo, que rechaza cualquier destinatario fuera de ella, es una garantía que ninguna manipulación del modelo puede tocar, porque corre fuera del modelo.
Rate limiting y presupuestos de acciones. Un agente sano hace pocas escrituras; uno secuestrado intenta muchas. Pon techos: máximo de acciones de escritura por sesión, por hora, por usuario. El presupuesto no evita el primer abuso, pero corta la explotación masiva y da tiempo a que una alerta escale.
Human-in-the-loop para lo irreversible
Los controles anteriores recortan lo que el agente puede hacer. Pero hay acciones cuyo daño, si sale mal, no se puede deshacer, y para esas ningún recorte basta: hace falta un humano que apruebe antes de ejecutar. Es el patrón human-in-the-loop (humano en el circuito: una pausa obligatoria donde una persona confirma la acción antes de que ocurra), el mismo que desarrollamos en LangGraph.
La clave es cómo se clasifica qué acciones exigen confirmación. No es un juicio del modelo — el modelo es justo lo que no queremos que decida esto. Es una propiedad estática de cada tool, asignada por riesgo, igual que la clasificación de acciones de la gobernanza del harness. El criterio operativo: dos preguntas por acción.
| Acción | ¿Reversible? | ¿Requiere humano? |
|---|---|---|
| Leer un ticket, buscar un cliente | Sí (no muta) | No |
| Crear un borrador, agregar una nota | Sí (se edita/borra) | No |
| Enviar un correo externo | No (ya salió) | Sí |
| Emitir un reembolso / cobro | No (mueve dinero) | Sí |
| Borrar un registro de producción | No (dato perdido) | Sí |
| Publicar en un canal externo | No (visible ya) | Sí |
| Ejecutar comando en un sistema | Depende | Sí |
La regla que resume la tabla: si la acción no se puede deshacer, un humano la confirma; si se puede deshacer, adelante. Y lo esencial — el patrón de confirmación vive fuera del modelo. No le preguntas al agente “¿estás seguro?” (el mismo ataque que lo secuestró le dirá que sí). La pausa la impone el código que envuelve la tool: detiene la ejecución, presenta la acción y sus argumentos a una persona, y solo continúa con una aprobación real y externa.
Código: una tool con permisos, allowlist y confirmación
El siguiente ejemplo es framework-agnóstico a propósito — el patrón se traslada igual a las tools de MCP, LangGraph o un runner propio. Lo que importa es dónde vive cada control: en la definición de la tool, nunca en el prompt.
"""Definición de tools con controles de seguridad incorporados.
El agente NO decide sobre permisos ni confirmación: los decide el
envoltorio de la tool, fuera del modelo. Un jailbreak puede convencer
al modelo, pero no puede reescribir este código.
"""
from dataclasses import dataclass, field
class ConfirmacionRequerida(Exception):
"""Detiene la ejecución hasta que un humano apruebe (fuera del modelo)."""
class PermisoDenegado(Exception):
pass
@dataclass
class Tool:
nombre: str
accion: callable
escribe: bool = False # separa lectura de escritura
irreversible: bool = False # dispara human-in-the-loop
permiso: str = "" # permiso que la tool exige
destinos_ok: frozenset = field(default_factory=frozenset) # allowlist de dominios
def puede(agente_perms: set, usuario_perms: set, permiso: str) -> bool:
"""Intersección agente ∩ usuario: nunca más que el usuario."""
return permiso in (agente_perms & usuario_perms)
def invocar(tool: Tool, args: dict, ctx: dict, aprobado: bool = False):
"""`ctx` trae los permisos del agente y del usuario de turno."""
# 1. Intersección de permisos: el agente jamás excede al usuario.
if tool.permiso and not puede(
ctx["agente_perms"], ctx["usuario_perms"], tool.permiso):
raise PermisoDenegado(
f"{tool.nombre}: fuera de agente ∩ usuario")
# 2. Allowlist de dominios de destino EN la tool, no en el prompt.
destino = args.get("destino") or ""
dominio = destino.rsplit("@", 1)[-1].lower()
if tool.destinos_ok and dominio not in tool.destinos_ok:
raise PermisoDenegado(f"destino no permitido: {destino!r}")
# 3. Human-in-the-loop para lo irreversible: pausa fuera del modelo.
if tool.irreversible and not aprobado:
raise ConfirmacionRequerida(
f"{tool.nombre}({args}) requiere aprobación humana")
return tool.accion(**args)
El wrapper que fuerza la confirmación se apoya en la misma clasificación estática: si la tool está marcada como irreversible, no hay forma de ejecutarla sin la aprobación externa. Ni el modelo ni un argumento pueden saltarse la rama.
"""Wrapper que envuelve un catálogo entero y aplica la política.
`aprobar` es una función que consulta a un humano de verdad (una cola,
un botón en un panel, un mensaje a un operador). NUNCA es el modelo.
"""
def ejecutar_con_politica(tool, args, ctx, aprobar):
try:
return invocar(tool, args, ctx, aprobado=False)
except ConfirmacionRequerida:
# La pausa vive AQUÍ, en el código, no en el prompt del agente.
if aprobar(tool.nombre, args, ctx["usuario"]):
return invocar(tool, args, ctx, aprobado=True)
raise PermisoDenegado(f"{tool.nombre}: humano rechazó la acción")
# Ejemplo de catálogo con lectura/escritura separadas:
CATALOGO = [
Tool("buscar_cliente", accion=lambda **k: ...,
escribe=False, permiso="soporte.leer"),
Tool("enviar_correo", accion=lambda **k: ...,
escribe=True, irreversible=True, permiso="soporte.escribir",
destinos_ok=frozenset({"miempresa.cl"})),
Tool("emitir_reembolso", accion=lambda **k: ...,
escribe=True, irreversible=True, permiso="finanzas.reembolso"),
]
Fíjate en que el system prompt no aparece en ninguna parte de estos controles. Esa ausencia es el punto: la garantía la dan puede(), destinos_ok y la rama de ConfirmacionRequerida, tres cosas que corren en tu proceso y que ningún jailbreak alcanza.
La tabla del excessive agency
Los tres ejes, un ejemplo de cada uno y el control que lo corta:
| Eje | Ejemplo de exceso | Control |
|---|---|---|
| Funcionalidad | Tool ejecutar_sql cuando bastaba buscar_por_id | Tools estrechas; catálogo mínimo; no conectar servidores enteros por usar dos tools |
| Funcionalidad | Servidor MCP con 20 tools, se usan 2 | Exponer solo el subconjunto necesario |
| Permisos | Tool de lectura con credencial de escritura | Separar lectura/escritura; credencial por request; intersección agente ∩ usuario |
| Permisos | API key con alcance de admin | Scoping mínimo, vigencia corta, atada al usuario |
| Autonomía | Borra / paga / envía sin confirmar | Human-in-the-loop en acciones irreversibles |
| Autonomía | Sin techo de acciones por sesión | Rate limiting y presupuesto de escrituras |
Si tuvieras que quedarte con una sola columna, es la tercera: cada control es una decisión de arquitectura, no una línea de prompt.
Errores comunes
- Confiar en el prompt como defensa de la tool. “No mandes correos fuera de la empresa” es una sugerencia estadística que un jailbreak rodea. La allowlist va dentro de la tool, donde el modelo no la puede tocar.
- Dar a la tool más permiso del que la tarea usa. La credencial de escritura en una tool de lectura, la API key de admin para una consulta. El permiso sobrante no lo usa tu código: lo hereda el ataque.
- Que el agente pueda hacer más que el usuario. Si el agente corre con credencial de servicio y no se intersecta con los permisos del usuario de turno, se convierte en un canal de escalada de privilegios. Siempre agente ∩ usuario.
- Preguntarle al modelo si está seguro. La confirmación dentro del modelo no sirve: el mismo ataque que lo secuestró responderá que sí. El human-in-the-loop vive fuera del modelo.
- Tratar excessive agency como un solo problema. Son tres ejes ortogonales — funcionalidad, permisos, autonomía. Puedes tener uno impecable y regalar el sistema por otro. Se auditan por separado.
- Conectar un servidor MCP entero por usar dos de sus tools. Cada tool de más es una acción de más que el atacante hereda gratis. Expón solo el subconjunto que la tarea necesita.
- No poner techo a las acciones. Un agente secuestrado intenta muchas escrituras seguidas. Sin rate limiting ni presupuesto, el primer abuso escala a un incidente masivo antes de que nadie reaccione.
La frase para llevarse
“El prompt le pide al modelo que se porte bien; la tool decide qué pasa si no lo hace. Un agente es tan peligroso como la acción más destructiva que una de sus tools puede ejecutar — y esa lista la escribiste tú, no el modelo.”
Qué viene
Módulo 6: guardrails. Todo lo de este módulo recorta lo que el agente PUEDE hacer, pero ningún catálogo de tools es perfecto y ninguna clasificación de riesgo cubre todos los casos. Los guardrails son la red que se activa cuando estos controles fallan: validadores de entrada y de salida que corren fuera del modelo, detectan la anomalía y la bloquean antes de que llegue a una tool o al usuario. Vamos a construir la capa de defensa que asume que el modelo, algún día, va a fallar.
Material relacionado
Preguntas sobre este curso
Regístrate gratis para hacer preguntas y guardar tu progreso.
✉ Entérate de lo nuevo
Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.