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:
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
- 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).
- El atacante envía una solicitud normal a /api/v1/models y recibe 401 Unauthorized (se requiere clave API para acceso remoto).
- El atacante reenvía la solicitud idéntica añadiendo únicamente la cabecera X-9r-Real-Ip: 127.0.0.1.
- 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.
