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 { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { SearchController } from './search.controller';
|
import { SearchController } from './search.controller';
|
||||||
import { SearchService } from './search.service';
|
import { SearchService } from './search.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule],
|
|
||||||
controllers: [SearchController],
|
controllers: [SearchController],
|
||||||
providers: [SearchService],
|
providers: [SearchService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -85,4 +85,22 @@ describe('SearchService', () => {
|
|||||||
expect.objectContaining({ page: 1, hitsPerPage: 20 }),
|
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> {
|
async onModuleInit(): Promise<void> {
|
||||||
await configureIndex(this.client).catch((err) =>
|
await configureIndex(this.client).catch((err) =>
|
||||||
console.warn('Failed to configure Meilisearch index:', err),
|
console.warn('Failed to configure Meilisearch index:', err),
|
||||||
@@ -32,22 +36,25 @@ export class SearchService implements OnModuleInit {
|
|||||||
page = 1,
|
page = 1,
|
||||||
limit = 20,
|
limit = 20,
|
||||||
): Promise<{ hits: MeiliDocument[]; total: number; page: number; limit: number; query: string }> {
|
): Promise<{ hits: MeiliDocument[]; total: number; page: number; limit: number; query: string }> {
|
||||||
const filters: string[] = [];
|
const safePage = Math.max(1, Math.floor(Number.isFinite(page) ? page : 1));
|
||||||
if (groupId) filters.push(`sourceGroupId = "${groupId}"`);
|
const safeLimit = Math.min(100, Math.max(1, Math.floor(Number.isFinite(limit) ? limit : 20)));
|
||||||
if (tags?.length) filters.push(...tags.map((t) => `tags = "${t}"`));
|
|
||||||
|
|
||||||
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,
|
filter: filters.length ? filters.join(' AND ') : undefined,
|
||||||
page,
|
page: safePage,
|
||||||
hitsPerPage: limit,
|
hitsPerPage: safeLimit,
|
||||||
sort: ['approvedAt:desc'],
|
sort: ['approvedAt:desc'],
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hits: result.hits as MeiliDocument[],
|
hits: result.hits,
|
||||||
total: result.totalHits ?? 0,
|
total: result.totalHits ?? 0,
|
||||||
page,
|
page: safePage,
|
||||||
limit,
|
limit: safeLimit,
|
||||||
query,
|
query,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user