fix(security): 鉴权签名密钥 fail-closed,禁用仓库内置默认密钥

- ADMIN_TOKEN_SECRET 的随机化闸门由 NODE_ENV 改为 SECURE_MODE:线上以 development 运行时不再使用公开默认密钥(该密钥可伪造管理员/用户 token)

- 运行模式与安全开关常量前移到模块顶部,避免鉴权常量初始化顺序问题

- 启动自检与回归用例同步(109 套件/720 用例全绿)
这个提交包含在:
xcbyy
2026-10-21 12:14:16 +08:00
父节点 dd836b674e
当前提交 40e601e345
共有 3 个文件被更改,包括 28 次插入 和 24 次删除

查看文件

@@ -216,8 +216,10 @@ const syncCurrentUserProfile = async () => {
...currentUserProfile.value, ...currentUserProfile.value,
...cached ...cached
}; };
const storedUid = Number(uni.getStorageSync('userId'));
if (!storedUid) return currentUserProfile.value;
try { try {
const remote = await getUserProfile(cached.id || 1001); const remote = await getUserProfile(storedUid);
if (remote && remote.nickname) { if (remote && remote.nickname) {
const merged = { const merged = {
...cached, ...cached,

查看文件

@@ -34,6 +34,24 @@ import { setWeChatRefundFn } from './core/pay/refund-bridge';
const app = express(); const app = express();
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
// ===== 运行模式与安全开关(必须最先定义:鉴权密钥/闸门依赖它们)=====
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
// 危险兜底必须显式开启:
// - ALLOW_INSECURE_DEV_AUTH=1:仅在本地联调时允许"信任客户端声明 userId"
// - ALLOW_SANDBOX_PAY=1:仅在联调时允许沙箱支付(无真实收款推进订单)
const ALLOW_INSECURE_DEV_AUTH = process.env.ALLOW_INSECURE_DEV_AUTH === '1';
const ALLOW_SANDBOX_PAY = process.env.ALLOW_SANDBOX_PAY === '1';
// 安全闸门不依赖 NODE_ENV(线上曾以 NODE_ENV=development 运行导致闸门全部失效):
// 默认按生产口径收紧,只有显式 ALLOW_INSECURE_DEV_AUTH=1 的本地联调才放开。
const SECURE_MODE = !ALLOW_INSECURE_DEV_AUTH;
// 订阅消息跳转版本:线上若以 NODE_ENV=development 运行,默认会落到 developer(真实用户收不到),
// 因此支持用 MINIPROGRAM_STATE=formal|trial|developer 显式指定。
const MINIPROGRAM_STATE = ((): 'formal' | 'trial' | 'developer' => {
const v = String(process.env.MINIPROGRAM_STATE || '').toLowerCase();
if (v === 'formal' || v === 'trial' || v === 'developer') return v;
return IS_PRODUCTION ? 'formal' : 'developer';
})();
// 反向代理(Nginx)后正确识别客户端 IP(登录限流等使用) // 反向代理(Nginx)后正确识别客户端 IP(登录限流等使用)
app.set('trust proxy', 1); app.set('trust proxy', 1);
// 隐藏技术栈标识 // 隐藏技术栈标识
@@ -251,9 +269,9 @@ const DEFAULT_ADMIN_TOKEN_SECRET = 'peiwan_admin_token_secret_v1';
const ADMIN_TOKEN_SECRET = (() => { const ADMIN_TOKEN_SECRET = (() => {
const provided = String(process.env.ADMIN_TOKEN_SECRET || '').trim(); const provided = String(process.env.ADMIN_TOKEN_SECRET || '').trim();
if (provided) return provided; if (provided) return provided;
if (process.env.NODE_ENV === 'production') { if (SECURE_MODE) {
console.error( console.error(
'[Peiwan Server][SECURITY] 生产环境未配置 ADMIN_TOKEN_SECRET,已临时改用进程内随机密钥(默认密钥签发的 token 一律失效)。' + '[Peiwan Server][SECURITY] 未配置 ADMIN_TOKEN_SECRET,已临时改用进程内随机密钥(默认密钥签发的 token 一律失效)。' +
'请尽快在环境变量中设置固定的强随机 ADMIN_TOKEN_SECRET(并建议单独设置 USER_TOKEN_SECRET),否则每次重启后台登录都会失效。' '请尽快在环境变量中设置固定的强随机 ADMIN_TOKEN_SECRET(并建议单独设置 USER_TOKEN_SECRET),否则每次重启后台登录都会失效。'
); );
return crypto.randomBytes(48).toString('base64url'); return crypto.randomBytes(48).toString('base64url');
@@ -2203,23 +2221,7 @@ async function resolveWechatPayRuntimeConfig(overrideCfg?: any) {
}; };
} }
// 生产环境支付沙箱开关:生产环境默认关闭「沙箱直接入账」,可显式设置 ALLOW_SANDBOX_PAY=1 放行 // 运行模式与安全开关已在文件顶部统一声明(鉴权常量依赖其初始化顺序),此处不再重复定义。
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
// 危险兜底必须显式开启:
// - ALLOW_INSECURE_DEV_AUTH=1:仅在本地联调时允许"信任客户端声明 userId"
// - ALLOW_SANDBOX_PAY=1:仅在联调时允许沙箱支付(无真实收款推进订单)
const ALLOW_INSECURE_DEV_AUTH = process.env.ALLOW_INSECURE_DEV_AUTH === '1';
const ALLOW_SANDBOX_PAY = process.env.ALLOW_SANDBOX_PAY === '1';
// 安全闸门不再依赖 NODE_ENV(线上曾以 NODE_ENV=development 运行导致闸门全部失效):
// 默认按生产口径收紧,只有显式 ALLOW_INSECURE_DEV_AUTH=1 的本地联调才放开。
const SECURE_MODE = !ALLOW_INSECURE_DEV_AUTH;
// 订阅消息跳转版本:线上若以 NODE_ENV=development 运行,默认会落到 developer(真实用户收不到),
// 因此支持用 MINIPROGRAM_STATE=formal|trial|developer 显式指定。
const MINIPROGRAM_STATE = ((): 'formal' | 'trial' | 'developer' => {
const v = String(process.env.MINIPROGRAM_STATE || '').toLowerCase();
if (v === 'formal' || v === 'trial' || v === 'developer') return v;
return IS_PRODUCTION ? 'formal' : 'developer';
})();
/** /**
* 确认某笔微信支付已成功:优先读本地意图,其次主动向微信支付查单(含短暂重试), * 确认某笔微信支付已成功:优先读本地意图,其次主动向微信支付查单(含短暂重试),
@@ -4950,8 +4952,8 @@ async function logStartupConfigSelfCheck(): Promise<void> {
const adminSecretSet = Boolean(String(process.env.ADMIN_TOKEN_SECRET || '').trim()); const adminSecretSet = Boolean(String(process.env.ADMIN_TOKEN_SECRET || '').trim());
const userSecretSet = Boolean(String(process.env.USER_TOKEN_SECRET || '').trim()); const userSecretSet = Boolean(String(process.env.USER_TOKEN_SECRET || '').trim());
if (isProd && !adminSecretSet) { if (SECURE_MODE && !adminSecretSet) {
console.warn('[Peiwan Config][WARN] 未设置 ADMIN_TOKEN_SECRET:已启用进程内随机密钥,重启后登录态失效;多实例部署务必设置固定密钥。'); console.warn('[Peiwan Config][WARN] 未设置 ADMIN_TOKEN_SECRET:已启用进程内随机密钥(默认公开密钥已禁用),重启后登录态失效;请尽快设置固定密钥。');
} else { } else {
console.log(`[Peiwan Config] ADMIN_TOKEN_SECRET=${adminSecretSet ? '已设置' : '开发默认值'} USER_TOKEN_SECRET=${userSecretSet ? '已设置' : '按 ADMIN 派生'}`); console.log(`[Peiwan Config] ADMIN_TOKEN_SECRET=${adminSecretSet ? '已设置' : '开发默认值'} USER_TOKEN_SECRET=${userSecretSet ? '已设置' : '按 ADMIN 派生'}`);
} }

查看文件

@@ -13,11 +13,11 @@ describe('第十八批鉴权密钥安全闸门', () => {
expect(indexCode).not.toContain("process.env.ADMIN_TOKEN_SECRET || 'peiwan_admin_token_secret_v1'"); expect(indexCode).not.toContain("process.env.ADMIN_TOKEN_SECRET || 'peiwan_admin_token_secret_v1'");
}); });
it('必须存在默认密钥常量与生产环境随机化闸门', () => { it('必须存在默认密钥常量与 fail-closed 随机化闸门(不依赖 NODE_ENV)', () => {
expect(indexCode).toContain('const DEFAULT_ADMIN_TOKEN_SECRET ='); expect(indexCode).toContain('const DEFAULT_ADMIN_TOKEN_SECRET =');
const start = indexCode.indexOf('const DEFAULT_ADMIN_TOKEN_SECRET ='); const start = indexCode.indexOf('const DEFAULT_ADMIN_TOKEN_SECRET =');
const block = indexCode.slice(start, start + 1200); const block = indexCode.slice(start, start + 1200);
expect(block).toContain("process.env.NODE_ENV === 'production'"); expect(block).toContain('if (SECURE_MODE)');
expect(block).toContain('crypto.randomBytes'); expect(block).toContain('crypto.randomBytes');
expect(block).toContain('SECURITY'); expect(block).toContain('SECURITY');
}); });