fix(security): 安全闸门与 NODE_ENV 解耦,支付/提现 fail-closed
- 新增 SECURE_MODE(仅 ALLOW_INSECURE_DEV_AUTH=1 时放开):后台登录口令、上传、内容检测、核销码归属全部改为默认收紧,修复线上以 NODE_ENV=development 运行时闸门失效 - 提现审核:未开通真实转账且未显式 ALLOW_SANDBOX_PAY=1 时一律 503,杜绝只改状态不打款 - 预支付:未开启沙箱时不再下发沙箱参数;小程序删除预支付失败伪造沙箱成功的兜底,改为抛错 - 订阅消息支持 MINIPROGRAM_STATE=formal|trial|developer 显式配置,避免真实用户收不到通知 - 动态列表/点赞身份统一 normalizeUserId;文档补充安全闸门与订阅消息环境变量 - 109 套件/720 用例全绿,tsc 构建通过
这个提交包含在:
@@ -11,10 +11,10 @@ describe('第七十二批后台凭证闸门', () => {
|
||||
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('生产未配置 ADMIN_PASSWORD 时禁用后台登录(fail-closed)', () => {
|
||||
it('未配置 ADMIN_PASSWORD 时禁用后台登录(fail-closed,不依赖 NODE_ENV)', () => {
|
||||
expect(indexCode).toContain('ADMIN_PASSWORD_CONFIGURED');
|
||||
expect(indexCode).toContain('if (IS_PRODUCTION && !ADMIN_PASSWORD_CONFIGURED)');
|
||||
expect(indexCode).toContain('生产环境未配置后台登录口令');
|
||||
expect(indexCode).toContain('if (SECURE_MODE && !ADMIN_PASSWORD_CONFIGURED)');
|
||||
expect(indexCode).toContain('后台登录口令未配置');
|
||||
});
|
||||
|
||||
it('启动自检提示默认口令与非生产放宽风险', () => {
|
||||
|
||||
@@ -9,11 +9,11 @@ describe('第四十六批内容安全接口闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
|
||||
it('生产需登录且 openid 以会话用户为准', () => {
|
||||
it('需登录且 openid 以会话用户为准(安全闸门不依赖 NODE_ENV)', () => {
|
||||
const start = indexCode.indexOf("app.post('/api/security/msg-sec-check'");
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = indexCode.slice(start, start + 1200);
|
||||
expect(block).toContain('IS_PRODUCTION');
|
||||
expect(block).toContain('SECURE_MODE');
|
||||
expect(block).toContain("'内容安全检测'");
|
||||
expect(block).toContain('await resolveUserOpenid(callerId)');
|
||||
expect(block).not.toContain('const { content, openid, scene } = req.body');
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('第二十二批登录安全闸门', () => {
|
||||
it('登录路由:超限锁定 + 常量时间比较', () => {
|
||||
const start = indexCode.indexOf("app.post('/api/user/login'");
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = indexCode.slice(start, start + 1200);
|
||||
const block = indexCode.slice(start, start + 2000);
|
||||
expect(block).toContain('isLoginLocked(loginKey)');
|
||||
expect(block).toContain('登录失败次数过多');
|
||||
expect(block).toContain('recordLoginFailure(loginKey)');
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('第七批安全加固闸门 (评价回复 / 服务上下架 / 通用
|
||||
describe('3. 通用核销归属 (生产闸门)', () => {
|
||||
it('生产环境必须校验核销人:活动=发起人,订单=承接大神', () => {
|
||||
const block = routeBlock("app.post('/api/verify/universal'");
|
||||
expect(block).toContain('IS_PRODUCTION && !isVerifiedAdminRequest(req)');
|
||||
expect(block).toContain('SECURE_MODE && !isVerifiedAdminRequest(req)');
|
||||
expect(block).toContain("info.type === 'ACTIVITY'");
|
||||
expect(block).toContain('Number(info.creatorId) === operator');
|
||||
expect(block).toContain('own!.companionIds.includes(operator)');
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第九十三批:安全闸门与 NODE_ENV 解耦 + 支付/提现 fail-closed。
|
||||
* 背景:线上以 NODE_ENV=development 运行,导致所有 `IS_PRODUCTION && ...` 闸门失效:
|
||||
* - 后台可用默认口令 admin/admin123 登录;
|
||||
* - 提现审核通过只改状态、不真实打款;
|
||||
* - 4 位核销码可被任意登录用户枚举/核销;
|
||||
* - 匿名可上传、可无限调用内容安全检测。
|
||||
* 另外小程序在预支付失败时会伪造"沙箱支付成功",必须改为抛错。
|
||||
*/
|
||||
describe('第九十三批安全闸门解耦与支付 fail-closed', () => {
|
||||
const root = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(root, 'server/src/index.ts'), 'utf-8');
|
||||
const apiCode = fs.readFileSync(path.join(root, 'pwuniapp/common/api.js'), 'utf-8');
|
||||
|
||||
const routeBlock = (marker: string, len = 3000) => {
|
||||
const start = indexCode.indexOf(marker);
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
return indexCode.slice(start, start + len);
|
||||
};
|
||||
|
||||
it('安全闸门统一由 SECURE_MODE 控制,不依赖 NODE_ENV', () => {
|
||||
expect(indexCode).toContain('const SECURE_MODE = !ALLOW_INSECURE_DEV_AUTH;');
|
||||
expect(routeBlock("app.post('/api/security/msg-sec-check'")).toContain('if (SECURE_MODE)');
|
||||
expect(indexCode).not.toContain('if (IS_PRODUCTION && !ADMIN_PASSWORD_CONFIGURED)');
|
||||
expect(indexCode).not.toContain('if (IS_PRODUCTION) {\n try {\n resolveSecureUserId');
|
||||
});
|
||||
|
||||
it('提现审核未开通真实转账时必须 503,禁止只改状态不打款', () => {
|
||||
const block = routeBlock("app.post('/api/finance/withdrawals/:id/review'", 6000);
|
||||
expect(block).not.toContain('IS_PRODUCTION && !ALLOW_SANDBOX_PAY');
|
||||
expect(block).toContain('if (!ALLOW_SANDBOX_PAY)');
|
||||
expect(block).toContain('微信商家转账未就绪');
|
||||
});
|
||||
|
||||
it('预支付在未开启沙箱时拒绝下发沙箱参数', () => {
|
||||
const block = routeBlock("app.post('/api/pay/wechat/prepay'", 12000);
|
||||
const sandboxIdx = block.indexOf("mode: 'SANDBOX'");
|
||||
expect(sandboxIdx).toBeGreaterThan(-1);
|
||||
expect(block.slice(0, sandboxIdx)).toContain('if (!ALLOW_SANDBOX_PAY)');
|
||||
});
|
||||
|
||||
it('订阅消息版本支持 MINIPROGRAM_STATE 显式配置', () => {
|
||||
expect(indexCode).toContain('process.env.MINIPROGRAM_STATE');
|
||||
expect(indexCode).toContain('const MINIPROGRAM_STATE = (');
|
||||
expect(indexCode).not.toContain("miniprogram_state: IS_PRODUCTION ? 'formal' : 'developer'");
|
||||
});
|
||||
|
||||
it('小程序预支付失败必须抛错,不得伪造沙箱支付成功', () => {
|
||||
expect(apiCode).not.toContain('SANDBOX_FALLBACK');
|
||||
expect(apiCode).toContain('微信支付发起失败,请稍后重试');
|
||||
expect(apiCode).toMatch(/catch \(err\) \{\n \/\/ 预支付失败必须向上抛出/);
|
||||
});
|
||||
|
||||
it('动态列表与点赞的身份统一走 normalizeUserId', () => {
|
||||
expect(apiCode).toContain('const uid = normalizeUserId(userId);\n let query = `?userId=${uid}`;');
|
||||
expect(apiCode).toContain('const uid = normalizeUserId(userId);\n const payload = { userId: uid };');
|
||||
});
|
||||
});
|
||||
@@ -9,11 +9,11 @@ describe('第四十五批上传鉴权闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
|
||||
it('生产环境上传必须要求登录', () => {
|
||||
it('上传必须要求登录(安全闸门不依赖 NODE_ENV)', () => {
|
||||
const start = indexCode.indexOf('const handleFileUpload =');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = indexCode.slice(start, start + 500);
|
||||
expect(block).toContain('IS_PRODUCTION');
|
||||
expect(block).toContain('SECURE_MODE');
|
||||
expect(block).toContain("resolveSecureUserId(req, 0, '上传文件')");
|
||||
expect(block).toContain('请先登录后再上传');
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('第十五批核销码与抢单池加固闸门', () => {
|
||||
it('必须要求登录并按门票归属方校验', () => {
|
||||
const block = routeBlock("app.get('/api/verify/query/:code'");
|
||||
expect(block).toContain("'查询核销码'");
|
||||
expect(block).toContain('IS_PRODUCTION && !isVerifiedAdminRequest(req)');
|
||||
expect(block).toContain('SECURE_MODE && !isVerifiedAdminRequest(req)');
|
||||
expect(block).toContain('无权查看该核销码信息');
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户