security: 危险兜底默认关闭(secure by default)——修复 NODE_ENV 未设导致的身份伪造/沙箱支付/隐私泄露
根因:线上 NODE_ENV 未设为 production 时,以下"开发兜底"全部自动生效: - resolveSecureUserId 信任客户端声明的 userId → 任意用户身份伪造 - ALLOW_SANDBOX_PAY = ... || !IS_PRODUCTION → 无真实收款即可推进订单(资损) - GET /api/users/:id 按"本人"返回余额/佣金/手机号 → 隐私泄露 修复(兜底改为必须显式开启): - 新增 ALLOW_INSECURE_DEV_AUTH(默认 false);resolveSecureUserId 改为 `if (IS_PRODUCTION || !ALLOW_INSECURE_DEV_AUTH) 401` - ALLOW_SANDBOX_PAY 改为仅 `process.env.ALLOW_SANDBOX_PAY === '1'` - assertCompanionActor 与用户资料"本人视角"兜底加 ALLOW_INSECURE_DEV_AUTH 限制 - /api/health 新增 mode 与 sandboxPayAllowed;启动自检打印两个危险开关并告警 - 文档补充危险开关说明(生产禁止开启) 验证:本地模拟 NODE_ENV 未设 → 通知/订单/下单 均 401、用户资料不泄露隐私、沙箱支付关闭; 开启 ALLOW_INSECURE_DEV_AUTH=1 后联调能力保留;production 模式行为一致。(冒烟 13/13) 新增 secure-default-auth.spec.ts(103 套件/696 用例全绿)
这个提交包含在:
@@ -407,7 +407,9 @@ function resolveSecureUserId(req: Request, claimedUserId: number, action = '操
|
||||
}
|
||||
return tokenUid;
|
||||
}
|
||||
if (IS_PRODUCTION) {
|
||||
// 身份兜底(信任客户端声明的 userId)默认关闭:必须显式设置 ALLOW_INSECURE_DEV_AUTH=1,
|
||||
// 且生产环境永不开放。避免"NODE_ENV 忘记设成 production"导致线上可任意伪造身份。
|
||||
if (IS_PRODUCTION || !ALLOW_INSECURE_DEV_AUTH) {
|
||||
const err: any = new Error(`请先登录后再${action}`);
|
||||
err.status = 401;
|
||||
err.code = 40100;
|
||||
@@ -458,8 +460,8 @@ function assertCompanionActor(
|
||||
if (own && own.ownerUserId != null && callerId === own.ownerUserId) {
|
||||
return callerId;
|
||||
}
|
||||
if (!IS_PRODUCTION) {
|
||||
// 本地联调:兼容未绑定 user_id 的历史大神数据
|
||||
if (!IS_PRODUCTION && ALLOW_INSECURE_DEV_AUTH) {
|
||||
// 本地联调:兼容未绑定 user_id 的历史大神数据(需显式开启 ALLOW_INSECURE_DEV_AUTH=1)
|
||||
if (own && own.companionId != null && callerId === own.companionId) return callerId;
|
||||
return callerId;
|
||||
}
|
||||
@@ -2201,7 +2203,11 @@ async function resolveWechatPayRuntimeConfig(overrideCfg?: any) {
|
||||
|
||||
// 生产环境支付沙箱开关:生产环境默认关闭「沙箱直接入账」,可显式设置 ALLOW_SANDBOX_PAY=1 放行
|
||||
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
|
||||
const ALLOW_SANDBOX_PAY = process.env.ALLOW_SANDBOX_PAY === '1' || !IS_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';
|
||||
|
||||
/**
|
||||
* 确认某笔微信支付已成功:优先读本地意图,其次主动向微信支付查单(含短暂重试),
|
||||
@@ -3650,8 +3656,8 @@ app.get('/api/users/:id', async (req: Request, res: Response) => {
|
||||
const isAdmin = isVerifiedAdminRequest(req);
|
||||
let isSelf = false;
|
||||
if (!isAdmin) {
|
||||
if (!IS_PRODUCTION) {
|
||||
// 本地联调兼容:非生产默认按本人视角返回
|
||||
if (!IS_PRODUCTION && ALLOW_INSECURE_DEV_AUTH) {
|
||||
// 本地联调兼容:仅在显式开启 ALLOW_INSECURE_DEV_AUTH=1 时按本人视角返回
|
||||
isSelf = true;
|
||||
} else {
|
||||
try {
|
||||
@@ -4854,7 +4860,7 @@ app.post('/api/distribution/bind', async (req: Request, res: Response) => {
|
||||
async function logStartupConfigSelfCheck(): Promise<void> {
|
||||
try {
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
console.log(`[Peiwan Config] 运行模式=${isProd ? 'production' : 'development'} ALLOW_SANDBOX_PAY=${ALLOW_SANDBOX_PAY}`);
|
||||
console.log(`[Peiwan Config] 运行模式=${isProd ? 'production' : 'development'} ALLOW_SANDBOX_PAY=${ALLOW_SANDBOX_PAY} ALLOW_INSECURE_DEV_AUTH=${ALLOW_INSECURE_DEV_AUTH}`);
|
||||
|
||||
const adminSecretSet = Boolean(String(process.env.ADMIN_TOKEN_SECRET || '').trim());
|
||||
const userSecretSet = Boolean(String(process.env.USER_TOKEN_SECRET || '').trim());
|
||||
@@ -4869,9 +4875,15 @@ async function logStartupConfigSelfCheck(): Promise<void> {
|
||||
console.warn('[Peiwan Config][WARN] 未设置有效的 ADMIN_PASSWORD(不得为 admin/admin123):生产环境后台登录已被禁用,请尽快设置后重启。');
|
||||
}
|
||||
|
||||
// 非生产环境身份校验放宽(允许客户端声明身份),禁止用于公网部署
|
||||
// 危险兜底自检:仅在显式开启时提示(默认全部关闭 = fail-closed)
|
||||
if (ALLOW_INSECURE_DEV_AUTH) {
|
||||
console.warn('[Peiwan Config][WARN] 已开启 ALLOW_INSECURE_DEV_AUTH=1:未登录请求将采用客户端声明的 userId,仅限本地联调,切勿用于公网部署!');
|
||||
}
|
||||
if (ALLOW_SANDBOX_PAY) {
|
||||
console.warn('[Peiwan Config][WARN] 已开启 ALLOW_SANDBOX_PAY=1:允许沙箱支付(无真实收款推进订单),仅限联调环境!');
|
||||
}
|
||||
if (!isProd) {
|
||||
console.warn('[Peiwan Config][WARN] 当前非 production:用户身份校验已放宽(未登录请求将采用客户端声明的 userId),仅限本地联调,切勿用于公网部署。');
|
||||
console.warn('[Peiwan Config][WARN] 当前 NODE_ENV 非 production:订阅消息将以 developer 状态下发、部分生产闸门不生效;请确认是否为预期环境。');
|
||||
}
|
||||
|
||||
const cfg: any = await resolveWechatPayRuntimeConfig();
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('第七十二批后台凭证闸门', () => {
|
||||
|
||||
it('启动自检提示默认口令与非生产放宽风险', () => {
|
||||
expect(indexCode).toContain('未设置有效的 ADMIN_PASSWORD');
|
||||
expect(indexCode).toContain('用户身份校验已放宽');
|
||||
expect(indexCode).toContain('ALLOW_INSECURE_DEV_AUTH');
|
||||
});
|
||||
|
||||
it('部署文档包含 ADMIN_PASSWORD', () => {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第八十七批:危险兜底默认关闭(secure by default)。
|
||||
* 背景:线上 NODE_ENV 未设为 production 时,
|
||||
* - resolveSecureUserId 会信任客户端声明的 userId(任意身份伪造)
|
||||
* - ALLOW_SANDBOX_PAY 为 true(无真实收款即可推进订单)
|
||||
* - GET /api/users/:id 会按"本人"返回余额/佣金/手机号
|
||||
* 修复:以上兜底全部改为必须显式开启 ALLOW_INSECURE_DEV_AUTH=1 / ALLOW_SANDBOX_PAY=1。
|
||||
*/
|
||||
describe('第八十七批危险兜底默认关闭闸门', () => {
|
||||
const root = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(root, 'server/src/index.ts'), 'utf-8');
|
||||
const doc = fs.readFileSync(path.join(root, 'docs/部署验收与1分钱支付闭环.md'), 'utf-8');
|
||||
|
||||
it('身份兜底需显式开启(生产或未开启均 401)', () => {
|
||||
expect(indexCode).toContain("const ALLOW_INSECURE_DEV_AUTH = process.env.ALLOW_INSECURE_DEV_AUTH === '1';");
|
||||
expect(indexCode).toContain('if (IS_PRODUCTION || !ALLOW_INSECURE_DEV_AUTH)');
|
||||
});
|
||||
|
||||
it('沙箱支付必须显式开启(不再按 NODE_ENV 自动放行)', () => {
|
||||
expect(indexCode).toContain("const ALLOW_SANDBOX_PAY = process.env.ALLOW_SANDBOX_PAY === '1';");
|
||||
expect(indexCode).not.toContain("process.env.ALLOW_SANDBOX_PAY === '1' || !IS_PRODUCTION");
|
||||
});
|
||||
|
||||
it('用户资料/大神资料的"本人视角"兜底同样需显式开启', () => {
|
||||
const matches = indexCode.match(/if \(!IS_PRODUCTION && ALLOW_INSECURE_DEV_AUTH\)/g) || [];
|
||||
expect(matches.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('启动自检提示危险开关状态', () => {
|
||||
expect(indexCode).toContain('已开启 ALLOW_INSECURE_DEV_AUTH=1');
|
||||
expect(indexCode).toContain('已开启 ALLOW_SANDBOX_PAY=1');
|
||||
expect(indexCode).toContain('ALLOW_INSECURE_DEV_AUTH=${ALLOW_INSECURE_DEV_AUTH}');
|
||||
});
|
||||
|
||||
it('部署文档说明这两个危险开关默认关闭', () => {
|
||||
expect(doc).toContain('ALLOW_INSECURE_DEV_AUTH=1');
|
||||
expect(doc).toContain('ALLOW_SANDBOX_PAY=1');
|
||||
expect(doc).toContain('生产环境绝对不要开启');
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ describe('第五十四批启动自检闸门', () => {
|
||||
it('提供启动自检并输出就绪状态(不泄露密钥)', () => {
|
||||
expect(indexCode).toContain('async function logStartupConfigSelfCheck(');
|
||||
const start = indexCode.indexOf('async function logStartupConfigSelfCheck(');
|
||||
const block = indexCode.slice(start, start + 1800);
|
||||
const block = indexCode.slice(start, start + 2600);
|
||||
expect(block).toContain('运行模式=');
|
||||
expect(block).toContain('实盘就绪=');
|
||||
expect(block).toContain('未设置 ADMIN_TOKEN_SECRET');
|
||||
|
||||
在新工单中引用
屏蔽一个用户