Resumen

9Router determina si una solicitud entrante se origina desde localhost confiando en la cabecera HTTP X-9r-Real-Ip. Esta cabecera está diseñada para ser generada y saneada exclusivamente por la capa custom-server.js incluida en el paquete, derivándola de la dirección del socket TCP. En modos de despliegue donde las solicitudes llegan directamente a Next.js (sin que la cabecera sea eliminada ni regenerada), un atacante remoto no autenticado puede simplemente enviar X-9r-Real-Ip: 127.0.0.1 y ser tratado como cliente local. Esto elude el requisito de clave API en la API LLM pública (/api/v1/*), otorgando acceso no autenticado a los recursos del proveedor configurados por el propietario de la instancia.

Componente afectado

  • src/dashboardGuard.js
  • isLocalRequest(): confía en la cabecera X-9r-Real-Ip suministrada por el cliente para decidir el origen de loopback
  • canAccessPublicLlmApi(): concede acceso a /api/v1/* cuando isLocalRequest() devuelve true, omitiendo la validación de clave API
  • Ruta afectada verificada: GET /api/v1/models
  • Versión del producto probada: 9router-app 0.5.4 (Next.js 16.2.9)

Causa raíz

La capa de autorización toma una decisión de seguridad basándose en una cabecera HTTP controlable por el cliente. isLocalRequest() lee X-9r-Real-Ip y, si su valor es una dirección de loopback (127.0.0.1), clasifica la solicitud como local. El diseño asume que esta cabecera solo puede ser establecida por el wrapper de confianza custom-server.js (que la deriva de la dirección de socket no falsificable y elimina cualquier copia entrante). Cuando la aplicación se sirve sin ese wrapper, Next.js pasa la cabecera suministrada por el atacante sin modificaciones, violando la suposición de confianza:

text
Entrada no confiable del cliente
        ↓
X-9r-Real-Ip: 127.0.0.1
        ↓
isLocalRequest()  → true
        ↓
canAccessPublicLlmApi()  → permitido (clave API no requerida)
        ↓
200 OK

Escenario de ataque

  1. La instancia se despliega en un modo que no utiliza custom-server.js, y la API LLM es accesible por el atacante (el bind por defecto es 0.0.0.0).
  2. El atacante envía una solicitud normal a /api/v1/models y recibe 401 Unauthorized (se requiere clave API para acceso remoto).
  3. El atacante reenvía la solicitud idéntica añadiendo únicamente la cabecera X-9r-Real-Ip: 127.0.0.1.
  4. La solicitud se clasifica como local, la verificación de clave API se omite y el atacante recibe 200 OK con el catálogo de modelos del propietario y acceso continuo a la API LLM.

Prueba de concepto

La única diferencia entre la solicitud base y la solicitud de explotación es la adición de la cabecera X-9r-Real-Ip: 127.0.0.1. La solicitud base devuelve 401 Unauthorized. La solicitud con la cabecera falsificada devuelve 200 OK con acceso completo a los recursos del proveedor configurado.

Impacto

Un atacante remoto no autenticado que pueda alcanzar el servicio puede eludir la aplicación de claves API en la API LLM pública y actuar como cliente local de confianza. Las consecuencias incluyen:

  • Uso no autorizado de las conexiones al proveedor LLM configuradas por el propietario
  • Consumo de créditos API de pago y pérdida económica para el propietario de la instancia
  • Abuso de cuentas de proveedores upstream a través del proxy
  • Enumeración de proveedores configurados y modelos disponibles

Remediación

  • No confiar en X-9r-Real-Ip (ni en ninguna cabecera X-9r-*) cuando se recibe directamente de clientes.
  • Derivar la dirección del cliente para autorización desde una fuente de nivel de transporte de confianza, como req.socket.remoteAddress, en lugar de una cabecera de solicitud.
  • Si custom-server.js es requerido por el modelo de seguridad, fallar de forma cerrada cuando su marcador de confianza esté ausente, y eliminar o rechazar explícitamente cualquier cabecera X-9r-* suministrada por el cliente en el perímetro.
  • Documentar los modos de inicio seguros y soportados para que la aplicación no se ejecute en una configuración donde la cabecera sea controlable por el atacante.