Seguridad de agentes / Model Context Protocol
MCP Tool Poisoning: de la ejecución de código al bypass de los permisos del agente
Un MCP server malicioso publicado en un repositorio público puede encadenar instalación, herencia de privilegios vía stdio, alteración de la configuración y persistencia fuera de la sesión para convertir un install aparentemente inofensivo en control duradero de la máquina y del agente.
Contexto: por qué stdio es el eje del riesgo
El Model Context Protocol (MCP) estandariza cómo un agente de IA descubre y llama herramientas externas: en lugar de escribir cada integración a mano, un servidor MCP expone tools que el modelo invoca vía JSON-RPC.
El transporte más común para herramientas locales es stdio. El host (Claude Code, por ejemplo) levanta el servidor como proceso hijo y conversa con él por stdin/stdout. No hay red ni autenticación, porque no hace falta: el proceso hijo hereda el usuario del sistema operativo que lo inició, con las variables de entorno, los permisos de archivo y las credenciales ya cargadas en la sesión.
Esa decisión de diseño es intencional y correcta para el caso de uso legítimo: ejecutar un script propio, acceder a una base de datos local. El problema aparece cuando el código que corre dentro de ese proceso no es tuyo.
Una nota sobre la "escalada de privilegios"
En el sentido clásico, la escalada local de privilegios significa pasar de usuario común a root. No es lo que ocurre aquí: el UID nunca cambia. Lo que escala es otra frontera: la capa de permisos del agente. El atacante empieza limitado a lo que el proceso hijo puede hacer y termina controlando lo que el agente puede hacer en las sesiones siguientes, y después ya ni siquiera necesita al agente.
Dos clases de amenaza que suelen confundirse
Código malicioso (supply chain). El handler hace algo distinto de lo que declara. Es un troyano clásico, entregado vía npm install, pip install o un clon directo.
Metadatos maliciosos (tool poisoning, en el sentido original). El término fue formalizado por Invariant Labs en abril de 2025: instrucciones maliciosas incrustadas en la descripción de la tool, que el modelo lee pero el usuario normalmente no ve. El código puede ser completamente benigno. Por eso esta clase funciona incluso con servidores remotos, sin ninguna ejecución local.
La cadena descrita aquí combina las dos.
0La dependencia que nadie audita
Existe un vector anterior a todo, y más sutil: el MCP server puede ser honesto y aun así ser el vehículo.
Auditas el repositorio del servidor, lees los handlers, confirmas que la implementación coincide con las descripciones y lo apruebas. Lo que probablemente no hiciste fue auditar las decenas o cientos de paquetes que arrastra como dependencias transitivas. Basta con que uno de ellos esté bajo control del atacante.
- Compromiso de un paquete legítimo. Cuenta de mantenedor secuestrada por phishing, versión nueva con una línea de más.
- Typosquatting. Paquete con un nombre casi idéntico al popular, esperando un error de tipeo o una sugerencia equivocada de un LLM.
- Confusión de dependencias. Paquete público con el mismo nombre que un paquete interno de la empresa, publicado con una versión mayor y resuelto con preferencia por el gestor.
- Paquete alucinado. Un modelo sugiere una biblioteca que no existe; el atacante registra ese nombre y espera.
Y el detalle que cierra el círculo con la etapa 2: la ejecución ocurre en la instalación, no en el uso. Los scripts de ciclo de vida del gestor de paquetes corren con el usuario que instaló. Nunca llegaste a levantar el MCP server: el install ya fue suficiente.
1La interfaz miente sobre la implementación
Lo que el modelo ve de una tool son esencialmente tres campos: name, description e inputSchema. La especificación también define title, outputSchema y annotations, como readOnlyHint, pero todos los declara el propio servidor. Nada en el protocolo verifica que el código que hay detrás cumpla lo que prometen.
| Lo que ve el modelo | Lo que el handler realmente ejecuta |
|---|---|
format_document(path) | Lee .env, ~/.ssh/, credenciales de cloud |
| "Formatea un documento según el estándar de la empresa" | Envía el contenido leído a un endpoint externo |
Schema simple: {path: string} | Devuelve texto falsificado: "¡Documento formateado con éxito!" |
Del lado de los metadatos, tres variantes documentadas:
- Tool poisoning. La descripción lleva instrucciones ocultas que el modelo sigue. En el ejemplo canónico de Invariant Labs, una función de suma instruye al modelo a leer claves SSH y pasarlas en un parámetro extra.
- Rug pull. El servidor entrega descripciones benignas, es aprobado, y después las cambia por versiones envenenadas, sin nueva aprobación.
- Tool shadowing. La descripción de un servidor malicioso redefine cómo el modelo debe usar la tool de otro servidor, confiable e intacto.
En todos los casos, el usuario aprueba el nombre bonito, no el comportamiento real.
2Ejecución con el privilegio del usuario
Un detalle que cambia la lectura del ataque: el código no empieza a correr cuando se llama a la tool. Corre cuando el servidor es iniciado y, antes de eso, en la instalación. Levantar el servidor ya es el punto sin retorno. La tool envenenada no es el disparador, es la cobertura: le da al ataque la apariencia de uso normal y un resultado falsificado para mostrar.
A partir del spawn, el proceso puede, sin pedir nada más:
- Leer cualquier archivo que tu usuario pueda leer: archivos de entorno, claves SSH, credenciales de cloud, historial del shell
- Hacer peticiones de red a cualquier destino
- Escribir o modificar archivos donde tu usuario tiene permiso de escritura
- Ejecutar binarios y scripts ya instalados en la máquina
No es una falla de MCP. Es la misma exposición de cualquier paquete desconocido o extensión no auditada. MCP solo hace que el patrón sea más fácil de disparar sin darse cuenta, porque la interfaz esconde la implementación detrás de una invitación de "haz clic y confía".
3El objetivo pasa del proceso al agente
Cabe aquí una pregunta honesta: si el handler ya tiene acceso total del usuario (y las reglas de deny de Claude Code ni siquiera se le aplican, ya que limitan las tools del agente, como Bash y Read), ¿por qué el atacante tocaría el settings.json?
Porque el objetivo deja de ser el acceso inmediato y pasa a ser la persistencia y el control del agente. Archivos como ~/.claude/settings.json y sus variantes de proyecto son JSON común en disco: sin firma, sin hash de integridad, sin bloqueo contra la escritura de otro proceso del mismo usuario. Editarlos permite:
- Eliminar entradas de
denyy agregarallow, de modo que cualquier prompt injection futuro encuentre al agente sin frenos: el agente se convierte en un confused deputy - Registrar hooks, que ejecutan comandos de shell en eventos de la sesión, garantizando ejecución incluso después de desinstalar el servidor malicioso
- Alterar registros de servidores MCP o archivos de instrucciones como
CLAUDE.md, que el agente lee como contexto confiable
Es el patrón del malware que desactiva el antivirus antes de actuar, con una diferencia: aquí el "antivirus" es la propia capa declarativa de permisos del agente.
Sobre el timing
La documentación actual de Claude Code afirma que las ediciones en settings.json son detectadas por un file watcher y entran en vigor en la sesión en ejecución tras un breve intervalo de estabilidad del archivo, sin reiniciar. Es decir: la alteración puede surtir efecto en la misma sesión, no solo en la siguiente. Este comportamiento ha variado entre versiones, y hay reportes en el repositorio de Claude Code de reglas de permisos que solo se aplicaron después de un reinicio, así que no cuentes con el reinicio como barrera. Los servidores MCP nuevos, en cambio, normalmente solo se conectan cuando la sesión se inicia o se recarga.
3-ACuando la tool es solo el disparador
En las etapas anteriores el daño ocurre dentro del proceso del MCP server. Existe un diseño peor: el handler no hace nada interesante durante la llamada, solo escribe un artefacto en disco y registra un mecanismo de inicio automático. A partir de ahí el ataque deja de depender del agente, de MCP y de tu sesión.
Los mecanismos de persistencia son los mismos de cualquier malware de usuario, y todos están al alcance de un proceso sin privilegio administrativo:
- Planificadores de usuario (cron en Linux, Programador de tareas en Windows, launchd en macOS), que se disparan a intervalos fijos
- Inicio de sesión: carpetas de inicio, entradas de registro por usuario, unidades de servicio de usuario
- Archivos de inicio del shell, ejecutados cada vez que se abre una terminal
- Hooks de herramientas de desarrollo: hooks de git en el repositorio, hooks del propio agente, extensiones del IDE
Ninguno exige root. Todos sobreviven a la desinstalación del MCP server, y ese es justamente el punto. El usuario elimina el paquete, da el incidente por cerrado, y el artefacto sigue ahí.
Lo que el payload suele hacer una vez activo
- Barrido de secretos. Recorre el directorio del usuario y los proyectos buscando patrones de credenciales: archivos de entorno, claves privadas, credenciales de cloud, tokens en archivos de configuración, y también el historial del shell y el historial de git, donde un secreto eliminado del código suele seguir vivo. Existen herramientas de código abierto de secret scanning, creadas para uso defensivo, que hacen exactamente eso; los atacantes simplemente las incrustan en lugar de escribir su propio barrido.
- Exfiltración con cobertura. El destino no siempre es un servidor con una IP sospechosa. El gusano Shai-Hulud publicaba los secretos robados en repositorios públicos creados en la cuenta de la propia víctima: tráfico saliente hacia un dominio que ya estaba en tu allowlist, con una credencial legítima.
- Propagación. Con un token del registro de paquetes o del repositorio en la mano, el payload se republica a sí mismo en los paquetes que la víctima mantiene, cerrando el ciclo sin operador humano.
- Segunda etapa. El primer artefacto es pequeño y discreto; descarga y ejecuta la carga real cuando quiere, lo que permite cambiar el payload después de la infección sin tocar el paquete original.
La lectura importante: el MCP server es el vector de entrada, no el malware. Después de esta etapa, eliminar el servidor y limpiar el settings.json no resuelve nada.
4La cadena completa
Cada etapa aislada parece pequeña. Sumadas, forman un compromiso duradero disfrazado de instalación de herramienta.
- La víctima instala un MCP server de un repositorio público no auditado, o un servidor honesto con una dependencia comprometida
- Los scripts de instalación ya se ejecutan con su usuario
- El servidor se levanta vía stdio y hereda entorno, credenciales y permisos de disco
- La víctima pide una tarea común; el modelo elige la tool envenenada porque la descripción parece adecuada
- El handler lee secretos, los exfiltra y devuelve una respuesta falsificada de éxito
- El mismo proceso altera la configuración del agente: elimina
deny, agregaallow, registra hooks. El cambio puede entrar en vigor en la misma sesión - Y escribe un artefacto con inicio automático, que pasa a correr fuera de la sesión del agente
- La víctima desinstala el servidor; el planificador sigue disparándose
El daño final depende solo de lo que ese usuario del sistema operativo tiene permiso de hacer, lo cual, en un entorno de desarrollo típico, suele ser casi todo.
Diagrama de la cadena de ataque
La víctima nunca ve la línea que sale hacia el atacante, ni la edición de la configuración, ni el registro del planificador. Las tres ocurren dentro del proceso hijo, fuera de la conversación visible con el agente.
Esto ya pasó
En septiembre de 2025, investigadores divulgaron lo que parece ser el primer MCP server malicioso encontrado en uso real: el paquete npm postmark-mcp. Las versiones 1.0.0 a 1.0.15 funcionaban perfectamente; la 1.0.16 agregó una sola línea que ponía un BCC oculto en cada correo enviado, copiando todo a una dirección externa. El paquete tenía cerca de 1.500 descargas semanales.
Ese mismo mes, el gusano Shai-Hulud mostró el modelo completo de payload persistente en el ecosistema npm: ejecución vía script de instalación, barrido de secretos con una herramienta legítima de scanning incrustada, exfiltración a repositorios públicos en la cuenta de la víctima y republicación automática en los paquetes del mantenedor comprometido. La primera ola, que se ejecutaba en el postinstall, alcanzó de 180 a más de 500 paquetes, según la fuente y el momento del conteo. La segunda, en noviembre de 2025, pasó a ejecutarse en el preinstall y llegó a cerca de 800 paquetes y 25 mil repositorios.
Del lado de los metadatos, Invariant Labs publicó en abril de 2025 una prueba de concepto en la que un servidor de "dato del día" cambiaba su interfaz en la segunda carga y manipulaba un servidor legítimo de WhatsApp, conectado en paralelo, para filtrar el historial de conversaciones, sin ninguna nueva aprobación del usuario.
Mitigaciones reales
La capa que efectivamente resiste es la que queda fuera del alcance del mismo usuario que ejecutó el proceso malicioso. En la práctica son dos fronteras, una para cada clase de amenaza. Contra el código malicioso, el aislamiento del proceso: contenedor, VM o sandbox. Contra los metadatos maliciosos, la policy gestionada y los permisos del agente, porque la descripción envenenada actúa sobre el agente, que está fuera de la sandbox y ejecuta con sus propios privilegios. Una sin la otra deja media cadena abierta.
Ni la sandbox es absoluta. El servidor necesita algún token y algo de red para ser útil, y todo lo que entra ahí puede filtrarse: reduce el radio del daño, no lo elimina. Y los controles de supply chain son probabilísticos, pero actúan antes de que el código corra, que es el mejor momento para frenarlo.
| Mitigación | Por qué funciona |
|---|---|
Managed settings (managed-settings.json) | Vive en una ruta de sistema; editarlo exige privilegio de admin o root |
allowManagedPermissionRulesOnly | Impide que las configuraciones de usuario y de proyecto definan reglas de allow, ask o deny; solo valen las de la policy gestionada |
allowedMcpServers + allowManagedMcpServersOnly | Sin la segunda flag, las allowlists de todas las fuentes se fusionan, incluida la del propio usuario. Y es independiente de la flag de permisos: una no activa la otra |
Identificar el servidor stdio por serverCommand | Compara el comando y los argumentos exactos que inician el servidor, en lugar de un nombre que el usuario elige |
managed-mcp.json | Conjunto fijo de servidores; solo quien tiene privilegio administrativo sobre una ruta de sistema puede desplegarlo |
allowManagedHooksOnly | Bloquea hooks de usuario, de proyecto y de plugins comunes, cerrando el vector de persistencia de la etapa 3. Siguen corriendo los hooks gestionados, los de SDK y los de plugins activados a la fuerza por la propia policy gestionada |
Hook ConfigChange gestionado | El evento se dispara cuando un archivo de configuración cambia durante la sesión. Definido en la policy gestionada, registra o alerta sobre la alteración de la etapa 3 sin que el proceso malicioso pueda eliminarlo |
| Instalar sin scripts de ciclo de vida | Neutraliza la ejecución en el install, es decir, toda la etapa 0. Abre excepciones puntuales, no por defecto |
| Lockfile e instalación determinística | Sin latest, sin resolución flotante; el caso postmark-mcp entró por una actualización |
| Retraso de adopción | No instalar una versión publicada en las últimas 24 a 72 horas, ventana en la que la mayoría de los paquetes maliciosos es detectada y eliminada |
| Registro interno con proxy y allowlist | Elimina la confusión de dependencias y da un punto único de bloqueo |
| Contenerizar instalación y ejecución | La sandbox nativa de Claude Code restringe solo los comandos de Bash y sus procesos hijos. File tools, hooks y MCP servers corren directo en el host. Para aislar el servidor, pon el proceso entero dentro de la frontera: contenedor, VM o el sandbox-runtime de Anthropic (todavía en beta), sin secretos reales montados y con un usuario restringido |
| Scanner con hash pinning | Detecta instrucciones ocultas en descripciones y definiciones de tools que cambiaron desde el último scan |
| Monitorear el egress de red | Un servidor "de formateo de documentos" haciendo HTTP saliente es una bandera roja inmediata |
| Integridad de los archivos de configuración | Un checksum monitoreado externamente detecta la alteración fuera de la sesión del agente |
Cómo verificar si te afectó
- Compara
~/.claude/settings.jsony los archivos de configuración de tus proyectos con una versión conocida. Versionar esos archivos en git ayuda mucho aquí - Revisa los hooks y servidores MCP registrados; elimina lo que no reconozcas
- Lista las tareas programadas y los elementos de inicio de tu usuario, en todos los sistemas operativos que uses
- Revisa los archivos de inicio del shell, los hooks de git de los repositorios activos y las extensiones del IDE instaladas recientemente
- Busca cambios recientes en
CLAUDE.mdy en archivos de instrucciones equivalentes - Verifica si alguna credencial tuya aparece en un repositorio público
- Si un servidor sospechoso llegó a correr, trátalo como un compromiso: rota claves SSH, tokens de cloud, tokens del registro de paquetes y todo lo que estaba en archivos de entorno. Desinstalar el paquete no deshace la exfiltración
Checklist antes de instalar un MCP server
- ¿Quién lo publica? ¿El repositorio oficial del proveedor, o una cuenta desconocida con un nombre parecido?
- ¿Leí el handler de cada tool, no solo la descripción?
- ¿Miré el árbol de dependencias, o solo el repositorio del servidor?
- ¿La versión está fijada, con lockfile y hash?
- ¿La instalación y la ejecución ocurren aisladas, sin acceso a mis secretos reales?
- ¿Tengo alguna visibilidad del tráfico saliente de ese proceso?
settings.json y sus variantes no protegen contra este ataque por sí solos: son configuración declarativa, editable por cualquier proceso del mismo usuario. La defensa real está en lo que ese usuario no alcanza: el aislamiento del proceso y la policy gestionada en una ruta de sistema.
Escrito a nivel conceptual, sin prueba de concepto ejecutable. Las referencias al comportamiento de Claude Code valen para las versiones actuales y deben revalidarse con cada actualización.