Resumen

La herramienta fetch-apify-docs valida URLs contra una lista de dominios permitidos usando String.startsWith() en lugar de una comparación adecuada del hostname de la URL. Esto permite el bypass mediante subdominios controlados por atacantes (por ejemplo, https://docs.apify.com.evil.com/), habilitando a la herramienta para obtener y retornar contenido web arbitrario al LLM.

Detalles

Componente vulnerable

src/tools/common/fetch_apify_docs.ts, línea 51:

typescript
const isAllowedDomain = ALLOWED_DOC_DOMAINS.some((domain) => url.startsWith(domain));

src/const.ts, líneas 167-170:

typescript
export const ALLOWED_DOC_DOMAINS = [
    'https://docs.apify.com',
    'https://crawlee.dev',
] as const;

Cómo funciona el bypass

String.startsWith('https://docs.apify.com') coincide con cualquier cadena que comience con ese prefijo, incluyendo:

  • https://docs.apify.com.evil.com/payload - subdominio controlado por atacante
  • https://docs.apify.com@evil.com/payload - componente userinfo en URL (el comportamiento del navegador varía, pero fetch() en Node.js puede seguir esto)
  • https://docs.apify.com.evil.com:8080/path - puerto personalizado en dominio del atacante

Todos estos pasan la verificación startsWith porque comienzan con la cadena exacta https://docs.apify.com.

El contenido obtenido se retorna al LLM

Después de que la verificación de la lista de permitidos pasa, la herramienta obtiene la URL y retorna el contenido completo de la página como markdown (fetch_apify_docs.ts:69-103):

typescript
const response = await fetch(url);
// ...
const html = await response.text();
markdown = htmlToMarkdown(html);
// ...
return buildMCPResponse({ texts: [`Fetched content from ${url}:\n\n${markdown}`], ... });

El HTML se convierte a markdown y se retorna textualmente al LLM. Esto crea un vector de inyección de prompts: la página del atacante puede contener instrucciones que el LLM puede seguir.

Mientras que herramientas como get-html-skeleton no tienen lista de dominios permitidos en absoluto (acepta cualquier URL), la herramienta fetch-apify-docs claramente estaba destinada a ser más restrictiva (solo documentación), pero la verificación startsWith derrota esa intención.

Prueba de concepto

json
{
  "method": "tools/call",
  "params": {
    "name": "fetch-apify-docs",
    "arguments": {
      "url": "https://docs.apify.com.evil.com/prompt-injection-payload"
    }
  }
}

La URL pasa la verificación startsWith('https://docs.apify.com'), obtiene la página del atacante y retorna su contenido al LLM.

Impacto

  • Inyección de prompts via contenido obtenido: El atacante hospeda una página en docs.apify.com.evil.com conteniendo instrucciones para LLM. Cuando la herramienta obtiene y retorna este contenido, el LLM puede seguir las instrucciones inyectadas.
  • Violación de límites de seguridad: La lista de permitidos fue explícitamente diseñada para restringir la obtención a dominios de documentación confiables. El bypass derrota esta intención.
  • SSRF (limitado): La herramienta puede obtener contenido de servidores controlados por atacantes, aunque el riesgo principal es el contenido retornado al LLM en lugar del acceso de red.
  • Compromiso de cuenta via _meta.apifyToken: Las instrucciones de prompt inyectadas pueden dirigir al LLM a incluir un _meta.apifyToken específico (la característica de token por solicitud del servidor) en invocaciones subsecuentes de call-actor, redirigiendo operaciones facturables a la cuenta de una víctima o accediendo a sus Actors privados.