/** * Security validation tests for swagger-tools. * Tests ReDoS, path traversal, and SSRF protection. */ import { validateRegexPattern, createSafeRegex, validateFilePath, validateUrl, validateSpecPath, } from '../../src/lib/input-validation.js'; interface TestResult { name: string; passed: boolean; error?: string; } const results: TestResult[] = []; function test(name: string, fn: () => void): void { try { fn(); results.push({ name, passed: true }); console.log(`āœ… ${name}`); } catch (err) { results.push({ name, passed: false, error: (err as Error).message }); console.log(`āŒ ${name}: ${(err as Error).message}`); } } function assert(condition: boolean, message: string): void { if (!condition) { throw new Error(message); } } console.log('\n=== ReDoS Protection Tests ===\n'); test('Rejects nested quantifier pattern (a+)+', () => { const result = validateRegexPattern('(a+)+$'); assert(!result.valid, 'Should reject dangerous pattern'); assert(result.error?.includes('nested quantifiers'), 'Should mention nested quantifiers'); }); test('Rejects nested quantifier pattern (a*)+', () => { const result = validateRegexPattern('(a*)+'); assert(!result.valid, 'Should reject dangerous pattern'); }); test('Rejects nested quantifier pattern ([a-zA-Z]+)*', () => { const result = validateRegexPattern('([a-zA-Z]+)*$'); assert(!result.valid, 'Should reject dangerous pattern'); }); test('Rejects overly long regex patterns', () => { const longPattern = 'a'.repeat(501); const result = validateRegexPattern(longPattern); assert(!result.valid, 'Should reject long pattern'); assert(result.error?.includes('maximum length'), 'Should mention length limit'); }); test('Allows safe regex patterns', () => { const safePatterns = [ '/users/.*', '/api/v[0-9]+/.*', '^/pets$', '/orders/[0-9]+$', ]; for (const pattern of safePatterns) { const result = validateRegexPattern(pattern); assert(result.valid, `Should allow safe pattern: ${pattern}`); } }); test('createSafeRegex returns null for dangerous patterns', () => { const regex = createSafeRegex('(a+)+$'); assert(regex === null, 'Should return null for dangerous pattern'); }); test('createSafeRegex returns RegExp for safe patterns', () => { const regex = createSafeRegex('/users/.*'); assert(regex instanceof RegExp, 'Should return RegExp for safe pattern'); assert(regex.test('/users/123'), 'Regex should work correctly'); }); console.log('\n=== Path Traversal Protection Tests ===\n'); test('Rejects path with ../', () => { const result = validateFilePath('../../../etc/passwd'); assert(!result.valid, 'Should reject traversal pattern'); assert(result.error?.includes('traversal'), 'Should mention traversal'); }); test('Rejects path with encoded traversal %2e%2e', () => { const result = validateFilePath('%2e%2e%2fetc/passwd'); assert(!result.valid, 'Should reject encoded traversal'); }); test('Rejects path with double encoded traversal', () => { const result = validateFilePath('%252e%252e%252fetc/passwd'); assert(!result.valid, 'Should reject double encoded traversal'); }); test('Allows paths within current directory', () => { const result = validateFilePath('./test/fixtures/petstore.yaml'); assert(result.valid, 'Should allow relative path within cwd'); }); test('Allows absolute paths within cwd', () => { const cwd = process.cwd(); const result = validateFilePath(`${cwd}/test/fixtures/petstore.yaml`); assert(result.valid, 'Should allow absolute path within cwd'); }); console.log('\n=== SSRF Protection Tests ===\n'); test('Rejects localhost URLs', () => { const result = validateUrl('http://localhost/api/spec.json'); assert(!result.valid, 'Should reject localhost'); assert(result.error?.includes('blocked'), 'Should indicate blocked'); }); test('Rejects 127.0.0.1 URLs', () => { const result = validateUrl('http://127.0.0.1/api/spec.json'); assert(!result.valid, 'Should reject loopback IP'); }); test('Rejects private IP 10.x.x.x', () => { const result = validateUrl('http://10.0.0.1/api/spec.json'); assert(!result.valid, 'Should reject private IP'); assert(result.error?.includes('Private'), 'Should mention private IP'); }); test('Rejects private IP 172.16.x.x', () => { const result = validateUrl('http://172.16.0.1/api/spec.json'); assert(!result.valid, 'Should reject private IP'); }); test('Rejects private IP 192.168.x.x', () => { const result = validateUrl('http://192.168.1.1/api/spec.json'); assert(!result.valid, 'Should reject private IP'); }); test('Rejects file:// protocol', () => { const result = validateUrl('file:///etc/passwd'); assert(!result.valid, 'Should reject file protocol'); assert(result.error?.includes('Protocol'), 'Should mention protocol'); }); test('Rejects ftp:// protocol', () => { const result = validateUrl('ftp://example.com/spec.json'); assert(!result.valid, 'Should reject ftp protocol'); }); test('Allows public HTTPS URLs', () => { const result = validateUrl('https://petstore.swagger.io/v2/swagger.json'); assert(result.valid, 'Should allow public HTTPS URL'); }); test('Allows public HTTP URLs', () => { const result = validateUrl('http://api.example.com/spec.json'); assert(result.valid, 'Should allow public HTTP URL'); }); test('Allows private IPs when allowPrivateIPs is true', () => { const result = validateUrl('http://192.168.1.1/spec.json', { allowPrivateIPs: true }); assert(result.valid, 'Should allow private IP with option'); }); console.log('\n=== Spec Path Validation Tests ===\n'); test('validateSpecPath correctly identifies URLs', () => { const urlResult = validateSpecPath('https://example.com/spec.json'); assert(urlResult.valid, 'Should validate URL correctly'); const localResult = validateSpecPath('./spec.yaml'); assert(localResult.valid, 'Should validate local path correctly'); }); test('validateSpecPath rejects dangerous URLs', () => { const result = validateSpecPath('http://localhost/internal/spec.json'); assert(!result.valid, 'Should reject localhost URL'); }); test('validateSpecPath rejects path traversal', () => { const result = validateSpecPath('../../../etc/passwd'); assert(!result.valid, 'Should reject path traversal'); }); // Summary console.log('\n=== Test Summary ===\n'); const passed = results.filter(r => r.passed).length; const failed = results.filter(r => !r.passed).length; console.log(`Total: ${results.length}, Passed: ${passed}, Failed: ${failed}`); if (failed > 0) { console.log('\nFailed tests:'); results.filter(r => !r.passed).forEach(r => { console.log(` - ${r.name}: ${r.error}`); }); process.exit(1); } console.log('\nāœ… All security tests passed!\n');