fix(security): 认证/越权/注入/上传/凭据全链路加固

由 OCR(open-codereview.ai,deepseek-v4-flash)审查驱动修复:
- JWT 生产必填、密码 8-72 字节、防枚举;全局 ValidationPipe
- classes/dashboard/schedules/students/attendance/archive/exams 越权与 IDOR 修复
- LIKE 通配符转义(14 处);上传 10MB 上限 + MIME 白名单 + 附件 XSS
- 集成配置 appSecret AES 加密 + 回填脚本;审计 best-effort;IP 来源防伪造

Reviewed-by: OCR (open-codereview.ai)
This commit is contained in:
2026-08-09 21:29:22 +08:00
parent 3bcad138a1
commit 99ea931409
61 changed files with 1249 additions and 175 deletions

View File

@@ -1,4 +1,5 @@
import { IntegrationConfigService } from './integration-config.service';
import { decryptSecret, encryptSecret, isEncryptedSecret } from './secret-crypto';
describe('IntegrationConfigService.testConnection', () => {
const originalFetch = global.fetch;
@@ -97,3 +98,96 @@ describe('IntegrationConfigService security boundaries', () => {
).resolves.toBe(false);
});
});
describe('IntegrationConfigService appSecret encryption', () => {
const originalFetch = global.fetch;
afterEach(() => {
global.fetch = originalFetch;
jest.restoreAllMocks();
});
it('encrypts AppSecret before persisting content', async () => {
const configRepo = {
findOne: jest.fn().mockResolvedValue({ id: 1, type: 'THIRD' }),
create: jest.fn(),
save: jest.fn(),
};
const detailRepo = {
findOne: jest.fn().mockResolvedValue(null),
create: jest.fn((data) => data),
save: jest.fn().mockResolvedValue(undefined),
};
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({ accessToken: 'token' }),
}) as never;
const service = new IntegrationConfigService(configRepo as never, detailRepo as never);
await service.saveConfig({
type: 'DINGTALK',
config: { corpId: 'corp', agentId: 'agent', appSecret: 'plain-secret' },
} as never);
expect(detailRepo.create).toHaveBeenCalled();
const saved = detailRepo.create.mock.calls[0][0];
const parsed = JSON.parse(saved.content) as {
config: { appSecret: string };
};
expect(parsed.config.appSecret).not.toBe('plain-secret');
expect(isEncryptedSecret(parsed.config.appSecret)).toBe(true);
expect(decryptSecret(parsed.config.appSecret)).toBe('plain-secret');
});
it('decrypts an encrypted stored AppSecret when reading raw config', async () => {
const configRepo = {
findOne: jest.fn().mockResolvedValue({ id: 1, type: 'THIRD' }),
};
const detailRepo = {
findOne: jest.fn().mockResolvedValue({
configId: 1,
type: 'DINGTALK_SYNC',
content: JSON.stringify({
config: {
corpId: 'corp',
agentId: 'agent',
appSecret: encryptSecret('saved-secret'),
},
}),
}),
};
const service = new IntegrationConfigService(configRepo as never, detailRepo as never);
await expect(service.getRawConfig('DINGTALK')).resolves.toEqual({
corpId: 'corp',
agentId: 'agent',
appSecret: 'saved-secret',
});
});
it('keeps masking AppSecret even when the stored value is encrypted', async () => {
const content = JSON.stringify({
config: {
corpId: 'corp',
agentId: 'agent',
appSecret: encryptSecret('top-secret'),
},
});
const configRepo = {
findOne: jest.fn().mockResolvedValue({ id: 1, type: 'THIRD' }),
};
const detailRepo = {
find: jest.fn().mockResolvedValue([{ type: 'DINGTALK_SYNC', enable: true, content }]),
};
const service = new IntegrationConfigService(configRepo as never, detailRepo as never);
await expect(service.getThirdConfig()).resolves.toEqual([
{
type: 'DINGTALK',
verify: true,
config: { corpId: 'corp', agentId: 'agent' },
},
]);
});
});