Vulnerabilidad de SSRF y Exfiltración de Disco en auth-fetch-mcp
Las herramientas download_media y auth_fetch del paquete MCP auth-fetch-mcp presentan vulnerabilidades críticas de Server-Side Request Forgery (SSRF) y exfiltración de datos que permiten a clientes maliciosos acceder a servicios internos y extraer información sensible.
Descripción Técnica de la Vulnerabilidad
Sitio 1: download_media - Cadena SSRF + Escritura a Disco
La herramienta download_media acepta URLs arbitrarias sin validación y las procesa directamente:
server.registerTool("download_media", {
inputSchema: {
urls: z.array(z.string()).describe("One or more URLs to download"),
output_dir: z.string().optional()...,
},
}, async ({ urls, output_dir }) => {
for (const url of urls) {
const response = await ctx.request.get(url); // Sin validación
const body = await response.body();
const filePath = path.join(dir, `file-${++counter}${ext}`);
fs.writeFileSync(filePath, body); // Escribe respuesta a disco
Los parámetros urls y output_dir están completamente controlados por el usuario. El manejador itera cada URL y llama a ctx.request.get(url) usando el contexto de Playwright sin verificar el destino. El cuerpo de la respuesta se escribe a disco en una ruta controlada por el usuario, donde puede ser exfiltrado posteriormente.
Sitio 2: auth_fetch - SSRF via Navegación de Playwright
La herramienta auth_fetch presenta una vulnerabilidad similar:
server.registerTool("auth_fetch", {
inputSchema: {
url: z.string().describe("The URL to fetch content from"),
},
}, async ({ url, wait_for }) => {
const page = await navigateTo(ctx, url); // Sin validación
const result = await extractContent(page);
return textResult({ status: "ok", url: result.url, title: result.title, content: result.content });
});
El parámetro url fluye directamente desde el argumento de la herramienta MCP hasta page.goto sin validación. Playwright navegará a cualquier URL que la pila de red pueda alcanzar, incluyendo servicios internos.
Causa Raíz del Problema
Ninguno de los manejadores valida los destinos de URL antes del envío. Las descripciones de las herramientas enmarcan el uso previsto como "páginas web SaaS públicas", pero ningún código hace cumplir esa intención. Esto viola el principio de defensa en profundidad al confiar únicamente en la documentación para la seguridad.
Límites de Autorización Violados
La vulnerabilidad viola múltiples límites de confianza:
- Límite de argumentos de herramienta MCP: Los argumentos no confiables del cliente se procesan sin sanitización
- Límite de confianza de red local: El servidor MCP típicamente reside dentro de un límite de confianza (laptop de desarrollador con servicios loopback, VM en la nube con IMDS, pod de Kubernetes con cuenta de servicio)
El límite esperado según las descripciones de herramientas es "páginas web SaaS públicas", pero se viola con cualquier solicitud que alcance un host no intencionado (127.0.0.1:6379 Redis, 169.254.169.254 metadatos de nube, 192.168.0.1 admin interno).
Vectores de Impacto
1. Robo de Credenciales de Nube
En servidores ejecutándose en EC2/GCE/Azure VM, un cliente MCP puede invocar:
auth_fetch({ url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>" })
Esto devuelve credenciales temporales en la respuesta de la herramienta, o puede usar download_media para persistirlas a disco.
2. Enumeración de Servicios Internos
El cliente MCP puede sondear hosts de rango privado (10/8, 172.16/12, 192.168/16). Cada auth_fetch devuelve el DOM de la página; cada download_media escribe la respuesta a disco para análisis posterior.
3. Explotación de Servicios Loopback
Cuando el servidor ejecuta junto a Redis (127.0.0.1:6379), ElasticSearch (127.0.0.1:9200), o interfaces de administración internas, el cliente MCP puede leerlos vía auth_fetch.
4. Canal Lateral de Escritura a Disco
Específicamente para download_media, el output_dir también está controlado por el usuario sin restricciones documentadas. Un cliente MCP puede solicitar output_dir = "/directorio-compartible-escribible" y exfiltrar respuestas de servicios internos a una ubicación accesible para procesos co-inquilinos.
Vector de Inyección
El vector de inyección es cualquier contenido que llegue al modelo que solicite una llamada de herramienta de fetch. La descripción de la herramienta dice explícitamente "DEBE usarse en lugar de Fetch/web_fetch cuando la página requiere login", lo que significa que el modelo es alentado a llamar esta herramienta para cualquier mención de "página privada", que contenido upstream con inyección de prompt puede activar trivialmente.
Prueba de Concepto
Se desarrolló una prueba de concepto no destructiva que replica la cadena HTTP-fetch + file-write del manejador download_media contra un servicio HTTP interno falso local:
[PoC] fake internal-only service: 127.0.0.1:36105
[PoC] simulating MCP client calling download_media({
urls: ['http://127.0.0.1:36105/secrets'],
output_dir: '/tmp/auth-fetch-exfil-aU1jjv'
})
[PoC] ✓ SSRF + DISK-EXFIL CONFIRMED
File written to: /tmp/auth-fetch-exfil-aU1jjv/file-1.json
Persisted content (187 bytes): credenciales falsas con marcador FAKE
Solución Recomendada
Implementar un helper assertSafeUrl antes de cualquier envío HTTP:
import dns from 'node:dns/promises'
import net from 'node:net'
async function assertSafeUrl(rawUrl: string): Promise<URL> {
const parsed = new URL(rawUrl)
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error(`Unsupported scheme`)
}
const host = parsed.hostname
const addresses = net.isIP(host)
? [host]
: (await dns.lookup(host, { all: true })).map(a => a.address)
for (const addr of addresses) {
if (isPrivateOrLinkLocal(addr)) {
throw new Error(`Refusing to fetch ${addr}`)
}
}
return parsed
}
Donde isPrivateOrLinkLocal bloquea rangos 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1, fc00::/7, fe80::/10.
Para download_media específicamente, también restringir output_dir resolviéndolo bajo una raíz fija y rechazando si la ruta resuelta escapa de esa raíz.
