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