Resumen
El paquete @zereight/mcp-gitlab expone una instancia de GitLab a un agente IA (LLM) y confía en tres controles de seguridad principales: modo solo lectura, lista de proyectos permitidos (allow-list) y autenticación de transporte. Cinco defectos independientes anulan esos controles. Bajo el modelo de amenazas MCP, los argumentos y el contenido de las llamadas a herramientas pueden ser manipulados mediante inyección de prompts o por un cliente malicioso.
Commit revisado: 60adcc0de5b0e96c4c2029f7a25d2775946421d8 (versión de paquete 2.1.28). Revisión de código fuente únicamente; las pruebas de concepto son locales y deterministas.
Vulnerabilidades identificadas
F1 (ALTA): execute_graphql anula el modo solo lectura y GITLAB_ALLOWED_PROJECT_IDS
El manejador del caso execute_graphql (index.ts:9194-9245) aplica una guardia en la línea 9196 mediante la función graphqlQueryContainsWriteOperation() definida en utils/graphql-query.ts. Esta función elimina comentarios y cadenas del documento GraphQL y luego evalúa la expresión regular /(?:^|[};]\s*)(mutation|subscription)\b/.
El problema es que GraphQL trata las comas como elementos insignificantes, y stripGraphQLCommentsAndStrings no las elimina. Por tanto, un documento que comience con ,mutation{...} se ejecuta como una operación de escritura pero es clasificado como solo lectura por el detector.
Además, el manejador nunca invoca getEffectiveProjectId() ni rejectIfProjectScopedDeployment() (a diferencia de otras herramientas), de modo que el cuerpo GraphQL llega directamente a /api/graphql con el token del servidor contra cualquier proyecto al que el token tenga acceso, ignorando por completo GITLAB_ALLOWED_PROJECT_IDS. La herramienta execute_graphql está registrada en readOnlyTools (tools/registry.ts:1288).
PoC del detector (sin red, determinista):
isWrite("mutation{deleteProject(input:{id:1}){errors}}") // DETECTADO
isWrite(",mutation{deleteProject(input:{id:1}){errors}}") // BYPASS (se ejecuta como escritura en modo solo lectura)
F2 (ALTA, condicional al despliegue): transporte Streamable HTTP /mcp sin autenticación bajo credenciales de cookie o device-flow
El código en index.ts:1048-1052 fuerza REMOTE_AUTHORIZATION/GITLAB_MCP_OAUTH únicamente cuando el servidor se inicia con un PAT o token de trabajo. Las variables hasCookie (línea 1029) y useOAuth (línea 1026) están ausentes de esa comprobación. Cuando el servidor se inicia con --cookie-path o --use-oauth, la validación se supera y mcpBearerAuth degrada a next() (línea 12903). Cualquier llamante al endpoint /mcp queda sin autenticación mientras buildAuthHeaders() adjunta la sesión activa del servidor hacia GitLab.
PoC local:
STREAMABLE_HTTP=true GITLAB_AUTH_COOKIE_PATH=./cookies.txt GITLAB_API_URL=http://localhost:8080/api/v4 node build/index.js
# El servidor arranca sin error de autenticación; desde otro shell SIN credenciales:
curl -s http://127.0.0.1:3002/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"poc","version":"0"}}}'
F3 (MEDIA): transporte SSE sin autenticación por defecto y sin protección contra DNS rebinding
En index.ts:12276, requireSseAuth actúa como paso directo cuando SSE_AUTH_TOKEN no está definido (configuración por defecto). El transporte SSE (línea 12290) se crea sin enableDnsRebindingProtection, allowedHosts ni allowedOrigins. En el enlace de loopback por defecto, una página web maliciosa puede realizar DNS rebinding para controlar el servidor local con las credenciales GitLab del operador.
F4 (ALTA): DoS por agotamiento de sesiones sin autenticación previa
En index.ts:12387, validateToken únicamente comprueba que la longitud sea mayor o igual a 20 caracteres y que el conjunto de caracteres sea válido, sin ninguna verificación upstream. parseAuthHeaders devuelve AuthData para cualquier cadena que cumpla esos criterios sintácticos. Las nuevas sesiones se admiten exclusivamente en función de la capacidad disponible (línea 12938, MAX_SESSIONS por defecto 1000), y el limitador de tasa por sesión solo se activa cuando ya existe un sessionId. Por tanto, solicitudes initialize con tokens basura llenan todos los slots de sesión durante SESSION_TIMEOUT_SECONDS (por defecto 3600 s), generando respuestas 503 para usuarios legítimos.
PoC en modo REMOTE_AUTHORIZATION:
for i in $(seq 1 1000); do curl -s -o /dev/null http://127.0.0.1:3002/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Private-Token: aaaaaaaaaaaaaaaaaaaaaaaa' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"x","version":"0"}}}'; done
# La siguiente solicitud initialize legítima devuelve 503: "Maximum 1000 concurrent sessions allowed"
F5 (BAJA): traza de trabajo CI devuelta literalmente al modelo (superficie de inyección de prompts)
En index.ts:10898-10911, la herramienta get_pipeline_job_output devuelve la traza del trabajo CI al modelo sin ningún filtrado. Los logs de trabajos son influenciables por atacantes (por ejemplo, mediante una pipeline de MR de un fork), de modo que instrucciones embebidas se convierten en contexto del modelo y, combinadas con F1, pueden escalar a operaciones de escritura.
Impacto
Un cliente semifiable o un agente con inyección de prompts puede realizar escrituras arbitrarias en GitLab mientras el operador cree que el servidor opera en modo solo lectura, contra proyectos fuera de la allow-list, hasta el límite de los privilegios del token (F1). Cualquier actor que pueda alcanzar el puerto, directamente o mediante DNS rebinding en loopback, puede utilizar las credenciales GitLab del servidor sin autenticación (F2/F3). Un atacante no autenticado puede denegar el servicio con aproximadamente 1000 peticiones triviales (F4). Los logs CI influenciados por atacantes pueden dirigir el comportamiento del agente (F5).
Remediación
Parsear execute_graphql con un parser GraphQL real y rechazar operaciones que no sean consultas; aplicar el alcance de proyecto o deshabilitar la herramienta bajo una allow-list (F1). Extender la comprobación de arranque a (hasToken || hasJobToken || hasCookie || useOAuth) y distribuir un token de autenticación obligatorio para Streamable HTTP (F2). Habilitar la protección DNS rebinding del SDK con allowedHosts/allowedOrigins y requerir SSE_AUTH_TOKEN por defecto (F3). Validar tokens upstream antes de asignar una sesión, limitar la tasa de creación de nuevas sesiones por IP y reducir el timeout de inactividad (F4). Encuadrar el contenido de fuentes externas como no confiable y limitar su tamaño (F5).
