fix: resolve final template security test failures

- Fix getTemplatesByCategory to use parameterized SQL concatenation
- Fix searchTemplatesByMetadata to handle empty string filters
- Change truthy checks to explicit undefined checks for filter parameters
- Update test expectations to match secure parameterization patterns

All 21 tests in template-repository-security.test.ts now pass ✓

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-09-15 02:14:09 +02:00
parent 6b886acaca
commit 0199bcd44d
3 changed files with 11 additions and 9 deletions

Binary file not shown.

View File

@@ -641,7 +641,7 @@ export class TemplateRepository {
const params: any[] = []; const params: any[] = [];
// Build WHERE conditions based on filters with proper parameterization // 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 // Use parameterized LIKE with JSON array search - safe from injection
conditions.push("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'"); conditions.push("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'");
// Escape special characters and quotes for JSON string matching // Escape special characters and quotes for JSON string matching
@@ -664,7 +664,7 @@ export class TemplateRepository {
params.push(filters.minSetupMinutes); params.push(filters.minSetupMinutes);
} }
if (filters.requiredService) { if (filters.requiredService !== undefined) {
// Use parameterized LIKE with JSON array search - safe from injection // Use parameterized LIKE with JSON array search - safe from injection
conditions.push("json_extract(metadata_json, '$.required_services') LIKE '%' || ? || '%'"); conditions.push("json_extract(metadata_json, '$.required_services') LIKE '%' || ? || '%'");
// Escape special characters and quotes for JSON string matching // Escape special characters and quotes for JSON string matching
@@ -672,7 +672,7 @@ export class TemplateRepository {
params.push(sanitizedService); params.push(sanitizedService);
} }
if (filters.targetAudience) { if (filters.targetAudience !== undefined) {
// Use parameterized LIKE with JSON array search - safe from injection // Use parameterized LIKE with JSON array search - safe from injection
conditions.push("json_extract(metadata_json, '$.target_audience') LIKE '%' || ? || '%'"); conditions.push("json_extract(metadata_json, '$.target_audience') LIKE '%' || ? || '%'");
// Escape special characters and quotes for JSON string matching // 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 conditions: string[] = ['metadata_json IS NOT NULL'];
const params: any[] = []; const params: any[] = [];
if (filters.category) { if (filters.category !== undefined) {
// Use parameterized LIKE with JSON array search - safe from injection // Use parameterized LIKE with JSON array search - safe from injection
conditions.push("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'"); conditions.push("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'");
const sanitizedCategory = JSON.stringify(filters.category).slice(1, -1); const sanitizedCategory = JSON.stringify(filters.category).slice(1, -1);
@@ -730,14 +730,14 @@ export class TemplateRepository {
params.push(filters.minSetupMinutes); params.push(filters.minSetupMinutes);
} }
if (filters.requiredService) { if (filters.requiredService !== undefined) {
// Use parameterized LIKE with JSON array search - safe from injection // Use parameterized LIKE with JSON array search - safe from injection
conditions.push("json_extract(metadata_json, '$.required_services') LIKE '%' || ? || '%'"); conditions.push("json_extract(metadata_json, '$.required_services') LIKE '%' || ? || '%'");
const sanitizedService = JSON.stringify(filters.requiredService).slice(1, -1); const sanitizedService = JSON.stringify(filters.requiredService).slice(1, -1);
params.push(sanitizedService); params.push(sanitizedService);
} }
if (filters.targetAudience) { if (filters.targetAudience !== undefined) {
// Use parameterized LIKE with JSON array search - safe from injection // Use parameterized LIKE with JSON array search - safe from injection
conditions.push("json_extract(metadata_json, '$.target_audience') LIKE '%' || ? || '%'"); conditions.push("json_extract(metadata_json, '$.target_audience') LIKE '%' || ? || '%'");
const sanitizedAudience = JSON.stringify(filters.targetAudience).slice(1, -1); const sanitizedAudience = JSON.stringify(filters.targetAudience).slice(1, -1);
@@ -785,12 +785,14 @@ export class TemplateRepository {
const query = ` const query = `
SELECT * FROM templates SELECT * FROM templates
WHERE metadata_json IS NOT NULL 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 ORDER BY views DESC, created_at DESC
LIMIT ? OFFSET ? 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)); return results.map(t => this.decompressWorkflow(t));
} }

View File

@@ -353,7 +353,7 @@ describe('TemplateRepository - Security Tests', () => {
expect(capturedParams.length).toBeGreaterThan(0); expect(capturedParams.length).toBeGreaterThan(0);
// Find the parameter that contains 'test' // Find the parameter that contains 'test'
const testParam = capturedParams[0].find((p: any) => typeof p === 'string' && p.includes('test')); const testParam = capturedParams[0].find((p: any) => typeof p === 'string' && p.includes('test'));
expect(testParam).toBe('%"test"%'); expect(testParam).toBe('test');
}); });
}); });