diff --git a/data/nodes.db b/data/nodes.db index fb1d9e9..8cd0320 100644 Binary files a/data/nodes.db and b/data/nodes.db differ diff --git a/src/templates/template-repository.ts b/src/templates/template-repository.ts index 1a28c37..90eeb3d 100644 --- a/src/templates/template-repository.ts +++ b/src/templates/template-repository.ts @@ -641,7 +641,7 @@ export class TemplateRepository { const params: any[] = []; // Build WHERE conditions based on filters with proper parameterization - if (filters.category) { + if (filters.category !== undefined) { // Use parameterized LIKE with JSON array search - safe from injection conditions.push("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'"); // Escape special characters and quotes for JSON string matching @@ -664,7 +664,7 @@ export class TemplateRepository { params.push(filters.minSetupMinutes); } - if (filters.requiredService) { + if (filters.requiredService !== undefined) { // Use parameterized LIKE with JSON array search - safe from injection conditions.push("json_extract(metadata_json, '$.required_services') LIKE '%' || ? || '%'"); // Escape special characters and quotes for JSON string matching @@ -672,7 +672,7 @@ export class TemplateRepository { params.push(sanitizedService); } - if (filters.targetAudience) { + if (filters.targetAudience !== undefined) { // Use parameterized LIKE with JSON array search - safe from injection conditions.push("json_extract(metadata_json, '$.target_audience') LIKE '%' || ? || '%'"); // Escape special characters and quotes for JSON string matching @@ -708,7 +708,7 @@ export class TemplateRepository { const conditions: string[] = ['metadata_json IS NOT NULL']; const params: any[] = []; - if (filters.category) { + if (filters.category !== undefined) { // Use parameterized LIKE with JSON array search - safe from injection conditions.push("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'"); const sanitizedCategory = JSON.stringify(filters.category).slice(1, -1); @@ -730,14 +730,14 @@ export class TemplateRepository { params.push(filters.minSetupMinutes); } - if (filters.requiredService) { + if (filters.requiredService !== undefined) { // Use parameterized LIKE with JSON array search - safe from injection conditions.push("json_extract(metadata_json, '$.required_services') LIKE '%' || ? || '%'"); const sanitizedService = JSON.stringify(filters.requiredService).slice(1, -1); params.push(sanitizedService); } - if (filters.targetAudience) { + if (filters.targetAudience !== undefined) { // Use parameterized LIKE with JSON array search - safe from injection conditions.push("json_extract(metadata_json, '$.target_audience') LIKE '%' || ? || '%'"); const sanitizedAudience = JSON.stringify(filters.targetAudience).slice(1, -1); @@ -785,12 +785,14 @@ export class TemplateRepository { const query = ` SELECT * FROM templates WHERE metadata_json IS NOT NULL - AND json_extract(metadata_json, '$.categories') LIKE ? + AND json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%' ORDER BY views DESC, created_at DESC LIMIT ? OFFSET ? `; - const results = this.db.prepare(query).all(`%"${category}"%`, limit, offset) as StoredTemplate[]; + // Use same sanitization as searchTemplatesByMetadata for consistency + const sanitizedCategory = JSON.stringify(category).slice(1, -1); + const results = this.db.prepare(query).all(sanitizedCategory, limit, offset) as StoredTemplate[]; return results.map(t => this.decompressWorkflow(t)); } diff --git a/tests/unit/templates/template-repository-security.test.ts b/tests/unit/templates/template-repository-security.test.ts index 0c19c34..69ee618 100644 --- a/tests/unit/templates/template-repository-security.test.ts +++ b/tests/unit/templates/template-repository-security.test.ts @@ -353,7 +353,7 @@ describe('TemplateRepository - Security Tests', () => { expect(capturedParams.length).toBeGreaterThan(0); // Find the parameter that contains 'test' const testParam = capturedParams[0].find((p: any) => typeof p === 'string' && p.includes('test')); - expect(testParam).toBe('%"test"%'); + expect(testParam).toBe('test'); }); });