Files
competition-website/scripts/_secret.cjs
qubiaobiao 89b9b2b0da 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。
2026-10-12 10:26:03 +08:00

67 lines
3.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 };