fix(api): escape filter values, clamp pagination, remove redundant ConfigModule import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 00:31:43 +05:30
parent 8ad5f737bd
commit e73d39b798
3 changed files with 34 additions and 11 deletions
@@ -1,10 +1,8 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { SearchController } from './search.controller';
import { SearchService } from './search.service';
@Module({
imports: [ConfigModule],
controllers: [SearchController],
providers: [SearchService],
})
@@ -85,4 +85,22 @@ describe('SearchService', () => {
expect.objectContaining({ page: 1, hitsPerPage: 20 }),
);
});
it('escapes double-quotes in groupId to prevent filter injection', async () => {
mockSearch.mockResolvedValue({ hits: [], totalHits: 0 });
await service.search('hello', 'grp"1"OR id EXISTS');
expect(mockSearch).toHaveBeenCalledWith(
'hello',
expect.objectContaining({ filter: 'sourceGroupId = "grp\\"1\\"OR id EXISTS"' }),
);
});
it('clamps page to minimum 1 and limit to maximum 100', async () => {
mockSearch.mockResolvedValue({ hits: [], totalHits: 0 });
await service.search('hello', undefined, undefined, 0, 999);
expect(mockSearch).toHaveBeenCalledWith(
'hello',
expect.objectContaining({ page: 1, hitsPerPage: 100 }),
);
});
});
+16 -9
View File
@@ -19,6 +19,10 @@ export class SearchService implements OnModuleInit {
);
}
private static escapeFilterValue(value: string): string {
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}
async onModuleInit(): Promise<void> {
await configureIndex(this.client).catch((err) =>
console.warn('Failed to configure Meilisearch index:', err),
@@ -32,22 +36,25 @@ export class SearchService implements OnModuleInit {
page = 1,
limit = 20,
): Promise<{ hits: MeiliDocument[]; total: number; page: number; limit: number; query: string }> {
const filters: string[] = [];
if (groupId) filters.push(`sourceGroupId = "${groupId}"`);
if (tags?.length) filters.push(...tags.map((t) => `tags = "${t}"`));
const safePage = Math.max(1, Math.floor(Number.isFinite(page) ? page : 1));
const safeLimit = Math.min(100, Math.max(1, Math.floor(Number.isFinite(limit) ? limit : 20)));
const result = await this.client.index(MESSAGES_INDEX).search(query, {
const filters: string[] = [];
if (groupId) filters.push(`sourceGroupId = "${SearchService.escapeFilterValue(groupId)}"`);
if (tags?.length) filters.push(...tags.map((t) => `tags = "${SearchService.escapeFilterValue(t)}"`));
const result = await this.client.index<MeiliDocument>(MESSAGES_INDEX).search(query, {
filter: filters.length ? filters.join(' AND ') : undefined,
page,
hitsPerPage: limit,
page: safePage,
hitsPerPage: safeLimit,
sort: ['approvedAt:desc'],
});
return {
hits: result.hits as MeiliDocument[],
hits: result.hits,
total: result.totalHits ?? 0,
page,
limit,
page: safePage,
limit: safeLimit,
query,
};
}