后端: 身份与归属鉴权收口; 资金安全(结算闸门/支付防篡改/报名费真实扣款/赠金/结算唯一入账/提现打款); 数据泄露与隐私; 登录与密钥安全(默认密钥闸门/登录限流); 可观测性; 启动自检. 微信: 商家转账到零钱提现打款闭环(含回调/查单/失败退回佣金); 订阅消息真实下发闭环与后台模板字段对齐. 后台: 财务/微信支付配置读取失败锁定; 看板/待办/走势去假数据; 打款与测试推送文案修正. 小程序: 请求层错误透传与登录过期提示; 多页失败反馈与乐观回滚; 帮助中心FAQ页面化. 测试: 新增 40+ 回归护栏与真实 MySQL 冒烟(75 套件/599 用例全绿).
117 行
5.4 KiB
TypeScript
117 行
5.4 KiB
TypeScript
// 已读取AGENTS.md
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { ContentSecurityService } from '../src/core/security/content-security.service';
|
|
|
|
/**
|
|
* 第三批高危项回归闸门:友圈动态越权删除 / 组局解散提权 / 微信内容安全真实对接
|
|
* 以「源码契约断言 + 服务行为断言」锁定安全边界,防止后续重构悄然后退。
|
|
*/
|
|
describe('第三批安全加固闸门 (动态越权删除 / 组局解散提权 / 真实微信内容安全)', () => {
|
|
const rootDir = path.resolve(__dirname, '../../');
|
|
const serverRepoPath = path.join(rootDir, 'server/src/db/repository.ts');
|
|
const serverIndexPath = path.join(rootDir, 'server/src/index.ts');
|
|
const secServicePath = path.join(rootDir, 'server/src/core/security/content-security.service.ts');
|
|
const pwuniappApiPath = path.join(rootDir, 'pwuniapp/common/api.js');
|
|
|
|
describe('1. 友圈动态仅作者本人可删除', () => {
|
|
it('deletePost 必须校验作者归属并拒绝越权删除', () => {
|
|
const code = fs.readFileSync(serverRepoPath, 'utf-8');
|
|
expect(code).toContain('async deletePost(');
|
|
expect(code).toContain('无权删除他人发布的动态');
|
|
expect(code).toContain('SELECT id, user_id FROM posts WHERE id = ? LIMIT 1');
|
|
expect(code).toContain('DELETE FROM posts WHERE id = ? AND user_id = ?');
|
|
});
|
|
|
|
it('删除路由必须要求显式身份,不得默认 1001', () => {
|
|
const code = fs.readFileSync(serverIndexPath, 'utf-8');
|
|
const start = code.indexOf("app.delete('/api/posts/:id'");
|
|
expect(start).toBeGreaterThan(-1);
|
|
const block = code.slice(start, start + 700);
|
|
expect(block).toContain("req.query.userId || req.body?.userId || req.headers['x-user-id'] || 0");
|
|
expect(block).toContain('await assertUserNotBanned(uid');
|
|
expect(block).not.toContain('|| 1001)');
|
|
});
|
|
|
|
it('小程序删除动态必须携带真实登录用户ID', () => {
|
|
const code = fs.readFileSync(pwuniappApiPath, 'utf-8');
|
|
const start = code.indexOf('export async function deletePost(');
|
|
expect(start).toBeGreaterThan(-1);
|
|
const block = code.slice(start, start + 400);
|
|
expect(block).toContain('normalizeUserId(userId)');
|
|
expect(block).not.toContain('userId = 1001');
|
|
});
|
|
});
|
|
|
|
describe('2. 组局解散的管理员特权必须来自服务端签名 token', () => {
|
|
it('cancel 路由严禁信任客户端 operatorRole / x-user-role 提权', () => {
|
|
const code = fs.readFileSync(serverIndexPath, 'utf-8');
|
|
expect(code).toContain('function isVerifiedAdminRequest(');
|
|
const start = code.indexOf("app.post('/api/activities/:id/cancel'");
|
|
expect(start).toBeGreaterThan(-1);
|
|
const block = code.slice(start, start + 900);
|
|
expect(block).toContain('isVerifiedAdminRequest(req)');
|
|
expect(block).not.toContain("operatorRole === 'admin'");
|
|
expect(block).not.toContain("req.headers['x-user-role']");
|
|
});
|
|
});
|
|
|
|
describe('3. 内容安全真实对接微信 msgSecCheck', () => {
|
|
it('服务必须提供可注入的远端真实审核器与异步检测方法', () => {
|
|
const code = fs.readFileSync(secServicePath, 'utf-8');
|
|
expect(code).toContain('setRemoteChecker');
|
|
expect(code).toContain('checkTextAsync');
|
|
expect(code).toContain('RemoteTextChecker');
|
|
});
|
|
|
|
it('index.ts 必须注册微信 msg_sec_check 真实调用并接入关键 UGC 路由', () => {
|
|
const code = fs.readFileSync(serverIndexPath, 'utf-8');
|
|
expect(code).toContain('contentSecurityService.setRemoteChecker');
|
|
expect(code).toContain('https://api.weixin.qq.com/wxa/msg_sec_check');
|
|
expect(code).toContain('version: 2');
|
|
expect(code).toContain('await contentSecurityService.checkTextAsync(');
|
|
});
|
|
|
|
it('checkTextAsync 在无远端审核器时降级本地词库:命中拦截、合规放行', async () => {
|
|
const svc = new ContentSecurityService();
|
|
const hit = await svc.checkTextAsync('同城美女上门特殊服务');
|
|
expect(hit.pass).toBe(false);
|
|
expect(hit.code).toBe(87014);
|
|
expect(hit.suggest).toBe('block');
|
|
const ok = await svc.checkTextAsync('今晚一起打王者荣耀五排,辅助位缺一');
|
|
expect(ok.pass).toBe(true);
|
|
expect(ok.code).toBe(0);
|
|
});
|
|
|
|
it('注入的远端审核器会被调用,risky 内容应被二次拦截', async () => {
|
|
const svc = new ContentSecurityService();
|
|
let called = 0;
|
|
svc.setRemoteChecker(async (text) => {
|
|
called += 1;
|
|
if (text.includes('远端拦截')) {
|
|
return { pass: false, code: 87014, label: 'WECHAT_RISKY', suggest: 'block' as const, message: '内容涉嫌违规,已被微信内容安全系统拦截' };
|
|
}
|
|
return null;
|
|
});
|
|
expect(svc.hasRemoteChecker()).toBe(true);
|
|
|
|
const risky = await svc.checkTextAsync('这条内容需要远端拦截处理');
|
|
expect(called).toBe(1);
|
|
expect(risky.pass).toBe(false);
|
|
expect(risky.label).toBe('WECHAT_RISKY');
|
|
|
|
const normal = await svc.checkTextAsync('正常合规文本内容');
|
|
expect(normal.pass).toBe(true);
|
|
});
|
|
|
|
it('远端审核器抛异常时必须 fail-open 降级本地,避免误杀正常内容', async () => {
|
|
const svc = new ContentSecurityService();
|
|
svc.setRemoteChecker(async () => {
|
|
throw new Error('wechat api down');
|
|
});
|
|
const r = await svc.checkTextAsync('今晚一起打王者荣耀五排');
|
|
expect(r.pass).toBe(true);
|
|
});
|
|
});
|
|
});
|