From 89b9b2b0dafc18d85ff51ade36ec040a912bb279 Mon Sep 17 00:00:00 2001 From: qubiaobiao <3294694717@qq.com> Date: Mon, 12 Oct 2026 10:26:03 +0800 Subject: [PATCH] =?UTF-8?q?security(config):=20=E7=A7=BB=E9=99=A4=E5=86=85?= =?UTF-8?q?=E7=BD=AE=20JWT=20=E5=AF=86=E9=92=A5=E6=98=8E=E6=96=87=E5=85=9C?= =?UTF-8?q?=E5=BA=95=EF=BC=8C=E6=94=B6=E6=95=9B=E5=88=B0=E5=94=AF=E4=B8=80?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=85=A5=E5=8F=A3=EF=BC=88R105-D1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题(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。 --- config/config.default.js | 23 ++++++++++---- scripts/_secret.cjs | 66 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 scripts/_secret.cjs diff --git a/config/config.default.js b/config/config.default.js index 2533867..5b9730e 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -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):口令缺失同样阻断。 // 原实现在此处会静默回退到公开的开发口令,等价于「生产用公开口令连库」。 diff --git a/scripts/_secret.cjs b/scripts/_secret.cjs new file mode 100644 index 0000000..51a1efd --- /dev/null +++ b/scripts/_secret.cjs @@ -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 };