fix: address critical security issues in template metadata

- Fix SQL injection vulnerability in template-repository.ts
  - Use proper parameterization with SQLite concatenation operator
  - Escape JSON strings correctly for LIKE queries
  - Prevent malicious SQL through filter parameters

- Add input sanitization for OpenAI API calls
  - Sanitize template names and descriptions before sending to API
  - Remove control characters and prompt injection patterns
  - Limit input length to prevent token abuse

- Lower temperature to 0.3 for consistent structured outputs

- Add comprehensive test coverage
  - 100+ new tests for metadata functionality
  - Security-focused tests for SQL injection prevention
  - Integration tests with real database operations

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-09-15 00:51:41 +02:00
parent 1e586c0b23
commit c18c4e7584
9 changed files with 2257 additions and 21 deletions

View File

@@ -56,7 +56,9 @@ describe('TemplateService', () => {
created_at: overrides.created_at || '2024-01-01T00:00:00Z',
updated_at: overrides.updated_at || '2024-01-01T00:00:00Z',
url: overrides.url || `https://n8n.io/workflows/${id}`,
scraped_at: '2024-01-01T00:00:00Z'
scraped_at: '2024-01-01T00:00:00Z',
metadata_json: overrides.metadata_json || null,
metadata_generated_at: overrides.metadata_generated_at || null
});
beforeEach(() => {
@@ -79,7 +81,9 @@ describe('TemplateService', () => {
getExistingTemplateIds: vi.fn(),
clearTemplates: vi.fn(),
saveTemplate: vi.fn(),
rebuildTemplateFTS: vi.fn()
rebuildTemplateFTS: vi.fn(),
searchTemplatesByMetadata: vi.fn(),
getSearchTemplatesByMetadataCount: vi.fn()
} as any;
// Mock the constructor
@@ -520,6 +524,114 @@ describe('TemplateService', () => {
});
});
describe('searchTemplatesByMetadata', () => {
it('should return paginated metadata search results', async () => {
const mockTemplates = [
createMockTemplate(1, {
name: 'AI Workflow',
metadata_json: JSON.stringify({
categories: ['ai', 'automation'],
complexity: 'complex',
estimated_setup_minutes: 60
})
}),
createMockTemplate(2, {
name: 'Simple Webhook',
metadata_json: JSON.stringify({
categories: ['automation'],
complexity: 'simple',
estimated_setup_minutes: 15
})
})
];
mockRepository.searchTemplatesByMetadata = vi.fn().mockReturnValue(mockTemplates);
mockRepository.getSearchTemplatesByMetadataCount = vi.fn().mockReturnValue(12);
const result = await service.searchTemplatesByMetadata({
complexity: 'simple',
maxSetupMinutes: 30
}, 10, 5);
expect(result).toEqual({
items: expect.arrayContaining([
expect.objectContaining({
id: 1,
name: 'AI Workflow',
metadata: {
categories: ['ai', 'automation'],
complexity: 'complex',
estimated_setup_minutes: 60
}
}),
expect.objectContaining({
id: 2,
name: 'Simple Webhook',
metadata: {
categories: ['automation'],
complexity: 'simple',
estimated_setup_minutes: 15
}
})
]),
total: 12,
limit: 10,
offset: 5,
hasMore: false // 5 + 10 >= 12
});
expect(mockRepository.searchTemplatesByMetadata).toHaveBeenCalledWith({
complexity: 'simple',
maxSetupMinutes: 30
}, 10, 5);
expect(mockRepository.getSearchTemplatesByMetadataCount).toHaveBeenCalledWith({
complexity: 'simple',
maxSetupMinutes: 30
});
});
it('should use default pagination parameters', async () => {
mockRepository.searchTemplatesByMetadata = vi.fn().mockReturnValue([]);
mockRepository.getSearchTemplatesByMetadataCount = vi.fn().mockReturnValue(0);
await service.searchTemplatesByMetadata({ category: 'test' });
expect(mockRepository.searchTemplatesByMetadata).toHaveBeenCalledWith({ category: 'test' }, 20, 0);
});
it('should handle templates without metadata gracefully', async () => {
const templatesWithoutMetadata = [
createMockTemplate(1, { metadata_json: null }),
createMockTemplate(2, { metadata_json: undefined }),
createMockTemplate(3, { metadata_json: 'invalid json' })
];
mockRepository.searchTemplatesByMetadata = vi.fn().mockReturnValue(templatesWithoutMetadata);
mockRepository.getSearchTemplatesByMetadataCount = vi.fn().mockReturnValue(3);
const result = await service.searchTemplatesByMetadata({ category: 'test' });
expect(result.items).toHaveLength(3);
result.items.forEach(item => {
expect(item.metadata).toBeNull();
});
});
it('should handle malformed metadata JSON', async () => {
const templateWithBadMetadata = createMockTemplate(1, {
metadata_json: '{"invalid": json syntax}'
});
mockRepository.searchTemplatesByMetadata = vi.fn().mockReturnValue([templateWithBadMetadata]);
mockRepository.getSearchTemplatesByMetadataCount = vi.fn().mockReturnValue(1);
const result = await service.searchTemplatesByMetadata({ category: 'test' });
expect(result.items).toHaveLength(1);
expect(result.items[0].metadata).toBeNull();
});
});
describe('formatTemplateInfo (private method behavior)', () => {
it('should format template data correctly through public methods', async () => {
const mockTemplate = createMockTemplate(1, {