From 40e601e34522105aa6115019f3a6cc17c1ee6d8a Mon Sep 17 00:00:00 2001 From: xcbyy <2578898740@qq.com> Date: Wed, 21 Oct 2026 12:14:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(security):=20=E9=89=B4=E6=9D=83=E7=AD=BE?= =?UTF-8?q?=E5=90=8D=E5=AF=86=E9=92=A5=20fail-closed=EF=BC=8C=E7=A6=81?= =?UTF-8?q?=E7=94=A8=E4=BB=93=E5=BA=93=E5=86=85=E7=BD=AE=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=AF=86=E9=92=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ADMIN_TOKEN_SECRET 的随机化闸门由 NODE_ENV 改为 SECURE_MODE:线上以 development 运行时不再使用公开默认密钥(该密钥可伪造管理员/用户 token) - 运行模式与安全开关常量前移到模块顶部,避免鉴权常量初始化顺序问题 - 启动自检与回归用例同步(109 套件/720 用例全绿) --- pwuniapp/pages/community/detail.vue | 4 ++- server/src/index.ts | 44 ++++++++++++++------------ server/tests/auth-secret-guard.spec.ts | 4 +-- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/pwuniapp/pages/community/detail.vue b/pwuniapp/pages/community/detail.vue index 3fb90ad..ed01e85 100644 --- a/pwuniapp/pages/community/detail.vue +++ b/pwuniapp/pages/community/detail.vue @@ -216,8 +216,10 @@ const syncCurrentUserProfile = async () => { ...currentUserProfile.value, ...cached }; + const storedUid = Number(uni.getStorageSync('userId')); + if (!storedUid) return currentUserProfile.value; try { - const remote = await getUserProfile(cached.id || 1001); + const remote = await getUserProfile(storedUid); if (remote && remote.nickname) { const merged = { ...cached, diff --git a/server/src/index.ts b/server/src/index.ts index 1c4bc71..2e0c3a5 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -34,6 +34,24 @@ import { setWeChatRefundFn } from './core/pay/refund-bridge'; const app = express(); 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(登录限流等使用) app.set('trust proxy', 1); // 隐藏技术栈标识 @@ -251,9 +269,9 @@ const DEFAULT_ADMIN_TOKEN_SECRET = 'peiwan_admin_token_secret_v1'; const ADMIN_TOKEN_SECRET = (() => { const provided = String(process.env.ADMIN_TOKEN_SECRET || '').trim(); if (provided) return provided; - if (process.env.NODE_ENV === 'production') { + if (SECURE_MODE) { console.error( - '[Peiwan Server][SECURITY] 生产环境未配置 ADMIN_TOKEN_SECRET,已临时改用进程内随机密钥(默认密钥签发的 token 一律失效)。' + + '[Peiwan Server][SECURITY] 未配置 ADMIN_TOKEN_SECRET,已临时改用进程内随机密钥(默认密钥签发的 token 一律失效)。' + '请尽快在环境变量中设置固定的强随机 ADMIN_TOKEN_SECRET(并建议单独设置 USER_TOKEN_SECRET),否则每次重启后台登录都会失效。' ); 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 { const adminSecretSet = Boolean(String(process.env.ADMIN_TOKEN_SECRET || '').trim()); const userSecretSet = Boolean(String(process.env.USER_TOKEN_SECRET || '').trim()); - if (isProd && !adminSecretSet) { - console.warn('[Peiwan Config][WARN] 未设置 ADMIN_TOKEN_SECRET:已启用进程内随机密钥,重启后登录态失效;多实例部署务必设置固定密钥。'); + if (SECURE_MODE && !adminSecretSet) { + console.warn('[Peiwan Config][WARN] 未设置 ADMIN_TOKEN_SECRET:已启用进程内随机密钥(默认公开密钥已禁用),重启后登录态失效;请尽快设置固定密钥。'); } else { console.log(`[Peiwan Config] ADMIN_TOKEN_SECRET=${adminSecretSet ? '已设置' : '开发默认值'} USER_TOKEN_SECRET=${userSecretSet ? '已设置' : '按 ADMIN 派生'}`); } diff --git a/server/tests/auth-secret-guard.spec.ts b/server/tests/auth-secret-guard.spec.ts index edb9ddb..2478b97 100644 --- a/server/tests/auth-secret-guard.spec.ts +++ b/server/tests/auth-secret-guard.spec.ts @@ -13,11 +13,11 @@ describe('第十八批鉴权密钥安全闸门', () => { 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 ='); const start = indexCode.indexOf('const DEFAULT_ADMIN_TOKEN_SECRET ='); 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('SECURITY'); });