security(config): 移除内置 JWT 密钥明文兜底,收敛到唯一解析入口(R105-D1)
问题(R104 卫生清理 H2 分类的唯二受版本控制命中):
config.default.js 原为
const DEFAULT_JWT_SECRET = 'competition_jwt_secret_key_2026_br!cs';
secret: process.env.JWT_SECRET || DEFAULT_JWT_SECRET
该常量为**已入库、任何人可读**的明文,且与 scripts/.creds.local.json 的
jwt_fallback_secret 同值 ⇒ 等价于「JWT 签名密钥公开 ⇒ 任意人可伪造 token
冒充任意身份」。它与仓库既有纪律(_creds.py / _db.cjs / 本文件:459 对
DB_PASSWORD 的「缺失即阻断」)不一致 —— 同族问题各自实现必然漂移。
修法:
· config/config.default.js
- 删除 DEFAULT_JWT_SECRET 常量与 `||` 兜底,改调唯一解析入口;
- 生产安全守卫中同步移除 `jwt.secret === DEFAULT_JWT_SECRET` 比对分支
(不再存在可比的兜底值;缺失时解析期即抛错)。
· 新增 scripts/_secret.cjs(与 _db.cjs / _creds.py 同一纪律)
① env JWT_SECRET → ② scripts/.creds.local.json:jwt_fallback_secret
→ ③ 明确失败(⛔ 刻意不提供明文兜底)
行为等价性:生产经 systemd EnvironmentFile、本地经 loadDeployEnv 从
deploy/.env.production 注入 JWT_SECRET,正常运行路径逐字不变;仅消除了
「密钥缺失时静默使用公开值」这一分支。
验证:
· 后端真实重启 ⇒ 配置加载成功,生效 jwt.secret 指纹与注入值一致;
· 端到端判别力(本地 7001 与生产 8023 各一次):新密钥签发 ⇒ 200,
旧密钥签发 ⇒ 401(auth.js jwt.verify 分支);
· scripts/verify_no_secrets.py:HIGH 0,判别力自证 17/17;
· scripts/verify_script_health.py:H1–H4 PASS。
This commit is contained in:
@@ -297,11 +297,19 @@ module.exports = (appInfo) => {
|
||||
// -----------------------------------------------------------------
|
||||
// JWT 鉴权配置
|
||||
// -----------------------------------------------------------------
|
||||
const DEFAULT_JWT_SECRET = 'competition_jwt_secret_key_2026_br!cs';
|
||||
// 🔴 2026-10-12(R105-D1,用户授权):移除内置明文兜底密钥。
|
||||
// 原实现:`const DEFAULT_JWT_SECRET = '<明文串>'` + `process.env.JWT_SECRET || DEFAULT_JWT_SECRET`
|
||||
// —— 密钥缺失时静默回退到一个**已入库、任何人可读**的常量,且该常量与
|
||||
// scripts/.creds.local.json 的 jwt_fallback_secret 同值
|
||||
// ⇒ 等价于「JWT 签名密钥公开 ⇒ 任意人可伪造 token 冒充任意身份」。
|
||||
// 现收敛到唯一解析入口 `backend/scripts/_secret.cjs`(与 _db.cjs / _creds.py 同一纪律):
|
||||
// ① env JWT_SECRET → ② scripts/.creds.local.json:jwt_fallback_secret → ③ 明确失败
|
||||
// 行为等价性:生产与本地均由注入链路提供密钥(deploy/.env.production 的 JWT_SECRET
|
||||
// 经 loadDeployEnv 注入 process.env)⇒ 正常运行路径逐字不变,仅消除了兜底分支。
|
||||
const { loadJwtSecret } = require('../scripts/_secret.cjs');
|
||||
config.jwt = {
|
||||
// JWT 签名密钥:优先读取环境变量 JWT_SECRET,缺省时回退到内置值(仅供本地开发)
|
||||
// 【测试模拟值,生产请通过环境变量 JWT_SECRET 替换为 64 位以上随机串,否则启动被守卫阻断】
|
||||
secret: process.env.JWT_SECRET || DEFAULT_JWT_SECRET,
|
||||
// JWT 签名密钥:唯一来源见 _secret.cjs 头部说明;⛔ 无明文兜底,缺失即明确失败
|
||||
secret: loadJwtSecret(),
|
||||
enable: true,
|
||||
// Token 有效期(秒),默认 7 天
|
||||
expiresIn: 7 * 24 * 60 * 60,
|
||||
@@ -446,11 +454,14 @@ module.exports = (appInfo) => {
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 生产环境安全守卫:禁止使用内置默认密钥上线
|
||||
// (由 APP_ENV=production 触发;测试环境允许使用模拟密钥)
|
||||
// (由 APP_ENV=production 触发)
|
||||
// -----------------------------------------------------------------
|
||||
if (config.isProduction) {
|
||||
const insecure = [];
|
||||
if (config.jwt.secret === DEFAULT_JWT_SECRET) insecure.push('JWT_SECRET');
|
||||
// 🔴 2026-10-12(R105-D1):JWT_SECRET 不再在此比对 —— 内置兜底常量已移除,
|
||||
// 解析统一走 backend/scripts/_secret.cjs;密钥缺失时 loadJwtSecret() 在配置
|
||||
// 加载期即抛错 ⇒ 任何环境都不可能"静默使用公开密钥",本守卫无需(也无法)
|
||||
// 再做「是否等于内置值」的判断。
|
||||
if (config.keys === DEFAULT_APP_KEYS) insecure.push('APP_KEYS');
|
||||
// 🔴 2026-09-23(入库口令处置 P3):口令缺失同样阻断。
|
||||
// 原实现在此处会静默回退到公开的开发口令,等价于「生产用公开口令连库」。
|
||||
|
||||
Reference in New Issue
Block a user