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:
66
scripts/_secret.cjs
Normal file
66
scripts/_secret.cjs
Normal file
@@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 后端配置共用:JWT 签名密钥解析(**唯一入口**)
|
||||
* ============================================================
|
||||
*
|
||||
* 为什么单独成文件(R105 / 2026-10-12,D1)
|
||||
* ------------------------------------------------------------
|
||||
* 原实现(config.default.js 旧 L300/L304)为:
|
||||
* const DEFAULT_JWT_SECRET = '<32 位明文串>';
|
||||
* secret: process.env.JWT_SECRET || DEFAULT_JWT_SECRET
|
||||
* 这是**可预测的明文兜底**:密钥缺失时静默使用一个**已入库、任何人可读**的常量,
|
||||
* 且该常量与 `scripts/.creds.local.json` 的 `jwt_fallback_secret` 同值 ⇒ 等价于
|
||||
* 「JWT 签名密钥公开」。它与仓库既有纪律直接冲突:
|
||||
* · `scripts/_creds.py` / `backend/scripts/_db.cjs` 早已统一为「缺凭据明确失败」;
|
||||
* · config.default.js:459 对 DB_PASSWORD 也已改为「缺失即阻断」。
|
||||
* 同族问题若各自实现,必然漂移(R104 的结论)⇒ 收敛到**唯一解析源**,不留兜底。
|
||||
*
|
||||
* 读取优先级(与 `_db.cjs` / `_creds.py` 同一纪律)
|
||||
* ------------------------------------------------------------
|
||||
* 1. 环境变量 JWT_SECRET(生产 systemd / 启动器注入优先;本地由 config.default.js
|
||||
* 顶部的 loadDeployEnv() 从 deploy/.env.production 注入,非跳过键)
|
||||
* 2. 仓库根 `scripts/.creds.local.json` 的 `jwt_fallback_secret`(本机开发,
|
||||
* .gitignore 已排除;也是 scripts/_fixtures.py 的末级兜底来源,两侧同源 ⇒
|
||||
* 自签 token 与后端验签必然一致)
|
||||
* 3. 明确失败 —— ⛔ **刻意不提供明文兜底**:抛错并给出修复指引。
|
||||
* 理由:密钥缺失属"注入链路断了"的真实故障,明确阻断远优于静默用公开值签名。
|
||||
*
|
||||
* ⚠️ 与 Python 侧 `scripts/_creds.py`(JWT_FALLBACK_SECRET)口径必须一致;
|
||||
* 如需增删键,两边同时改。
|
||||
*
|
||||
* @returns {string} 生效中的 JWT 签名密钥
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const LOCAL_FILE = path.join(__dirname, '..', '..', 'scripts', '.creds.local.json');
|
||||
|
||||
function _loadLocal() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(LOCAL_FILE, 'utf8'));
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') return {}; // 文件不存在合法(走环境变量)
|
||||
// 损坏必须报错,不静默降级
|
||||
throw new Error(`凭据文件解析失败 ${LOCAL_FILE}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function loadJwtSecret() {
|
||||
const fromEnv = process.env.JWT_SECRET;
|
||||
if (fromEnv) return fromEnv;
|
||||
|
||||
const local = _loadLocal();
|
||||
const fromFile = local.jwt_fallback_secret;
|
||||
if (fromFile) return fromFile;
|
||||
|
||||
throw new Error(
|
||||
'[配置缺失] 未取到 JWT_SECRET —— 无法安全地签名/校验登录态。请任选其一:\n' +
|
||||
' ① 设置环境变量 JWT_SECRET(生产/CI 推荐,deploy/.env.production 已含该键);\n' +
|
||||
` ② 在 ${LOCAL_FILE} 写入键 "jwt_fallback_secret"(本机开发用,该文件已被 .gitignore 排除);\n` +
|
||||
'⛔ 本模块刻意不提供明文兜底:原 config.default.js 的内置密钥已移除,\n' +
|
||||
' 因为「可预测的公开密钥」等价于「JWT 可被任意伪造」。'
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { loadJwtSecret };
|
||||
Reference in New Issue
Block a user