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:
@@ -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' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user