Resumen

9router expone un proxy LLM compatible con OpenAI/Anthropic. El acceso remoto a este proxy está diseñado para estar protegido por una verificación de API key en el middleware de Next.js.

Sin embargo, 9router también define una reescritura que mapea /codex/* al endpoint backend LLM /api/v1/responses. La decisión de autorización del middleware se toma sobre la ruta de la solicitud entrante antes de que se aplique la reescritura. Dado que /codex no está incluido en la lista de prefijos de API LLM protegidos del middleware, las solicitudes a /codex/* eluden el control de API key y son posteriormente reescritas al mismo backend utilizado por /api/v1/responses.

Como resultado, un atacante remoto no autenticado puede acceder al proxy LLM a través de /codex/* y provocar que el servidor realice llamadas a proveedores upstream utilizando las credenciales del proveedor LLM almacenadas por el operador.

Detalles

Los componentes afectados son los siguientes:

  • Control de autorización del middleware (src/dashboardGuard.js): protege /v1, /v1beta, /api/v1 y /api/v1beta, pero no /codex.
  • Configuración de reescritura (next.config.mjs): reescribe /codex/:path* a /api/v1/responses.
  • Ruta del backend LLM (src/app/api/v1/responses/route.js): despacha las solicitudes reescritas al manejador LLM.
  • Manejador de chat (src/sse/handlers/chat.js): utiliza las credenciales del proveedor almacenadas por el operador para las llamadas upstream.

Versión probada:

Versión / CommitRuntimeEstado
v0.4.80, commit 23da7b1fe3bb8edd2bdbdb63fbbb15a476b02c56Next.js 16.2.9Afectado

Causa Raíz

El middleware clasifica las solicitudes por el pathname entrante original. Los prefijos de API LLM pública protegidos son:

js
PUBLIC_PREFIXES = ["/v1", "/v1beta", "/api/v1", "/api/v1beta"];

Dado que /codex no está incluido en esta lista, una solicitud como /codex/x no entra en la rama de autorización de la API LLM y cae hacia:

js
return NextResponse.next();

La configuración de reescritura mapea entonces la solicitud permitida a la ruta del backend protegido:

js
{
  source: "/codex/:path*",
  destination: "/api/v1/responses"
}

La ruta del backend alcanza el mismo manejador utilizado por el endpoint LLM canónico:

js
return await handleChat(request);

El manejador procesa la solicitud y realiza la llamada al proveedor LLM upstream. En la configuración probada, el manejador no repite el mismo control de API key del middleware para llamantes remotos, por lo que la solicitud reescrita es atendida tras eludir el control de autorización previsto.

Prueba de Concepto

Las siguientes solicitudes utilizan el mismo servidor objetivo y el mismo encabezado Host de estilo remoto. La única diferencia significativa es la ruta de la solicitud.

Caso 01: El endpoint canónico protegido rechaza el acceso no autenticado

http
POST /api/v1/responses HTTP/1.1
Host: evil.attacker.com
Content-Type: application/json
Content-Length: 156

{"model":"fakeoai/x","input":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello","messages":[{"role":"user","content":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello"}]}

Resultado observado:

http
HTTP/1.1 401 Unauthorized

Esto confirma que la ruta canónica /api/v1/responses está protegida por el control de API key previsto.

Caso 02: La ruta reescrita /codex/* elude el control de API key

http
POST /codex/x HTTP/1.1
Host: evil.attacker.com
Content-Type: application/json
Content-Length: 156

{"model":"fakeoai/x","input":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello","messages":[{"role":"user","content":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello"}]}

Resultado observado:

http
HTTP/1.1 200 OK

La solicitud alcanza el backend LLM sin API key. Un endpoint de proveedor upstream controlado registró la solicitud saliente de 9router:

text
POST /responses
Authorization: Bearer NINEROUTER_OPERATOR_STORED_KEY_MARKER
request-body marker present: true
operator key marker in Authorization: true

Esto confirma que la solicitud no autenticada a /codex/* provoca que 9router realice una llamada al proveedor upstream utilizando las credenciales almacenadas por el operador.

Caso 03: Una ruta desconocida no relacionada no alcanza el backend

http
POST /notcodex/x HTTP/1.1
Host: evil.attacker.com
Content-Type: application/json
Content-Length: 156

{"model":"fakeoai/x","input":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello","messages":[{"role":"user","content":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello"}]}

Resultado observado:

http
HTTP/1.1 404 Not Found

No se realiza ninguna llamada al proveedor upstream. Esto aísla el problema a la reescritura de /codex/*.

Caso 04: El endpoint canónico funciona correctamente con una API key válida

http
POST /api/v1/responses HTTP/1.1
Host: evil.attacker.com
Authorization: Bearer sk-REDACTED
Content-Type: application/json
Content-Length: 156

{"model":"fakeoai/x","input":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello","messages":[{"role":"user","content":"NINEROUTER_CODEX_AUTH_BYPASS_MARKER hello"}]}

Resultado observado:

http
HTTP/1.1 200 OK

Esto confirma que el endpoint canónico es funcional y que la respuesta 401 del Caso 01 es un fallo de autorización, no un error del backend.

Escenario de Ataque

  1. Un atacante remoto identifica una instancia de 9router accesible públicamente.
  2. El atacante envía solicitudes al proxy LLM a /codex/* en lugar de /api/v1/responses.
  3. El middleware evalúa la ruta original /codex/* y no aplica el control de API key LLM.
  4. La reescritura mapea la solicitud a /api/v1/responses.
  5. El backend procesa la solicitud y realiza una llamada al proveedor upstream.
  6. La llamada upstream utiliza las credenciales del proveedor almacenadas por el operador.

Impacto

Un atacante que explote esta vulnerabilidad con éxito puede utilizar la cuenta del proveedor LLM configurada por el operador sin autenticación.

Las consecuencias probables incluyen:

  • Uso no autorizado del proxy LLM de 9router.
  • Consumo de los créditos o cuota del proveedor del operador.
  • Impacto inesperado en la facturación.
  • Abuso de los proveedores compatibles con OpenAI/Anthropic configurados.
  • Exposición del comportamiento del modelo y del proveedor a través de las respuestas del proxy.
  • Elusión del control de acceso por API key previsto para el acceso remoto al proxy LLM.