swagger-tools/scripts/self-testing/mcp-integration-test.ts
rimskij d7f354b37d feat: add security hardening for ReDoS, path traversal, and SSRF
- Add input-validation.ts with regex, path, and URL validation utilities
- Validate regex patterns before RegExp creation to prevent ReDoS
- Block dangerous nested quantifiers (a+)+, (a*)+, etc.
- Prevent path traversal with directory escape detection
- Block localhost, private IPs, and non-http/https protocols for SSRF
- Add SecurityOptions for configurable validation (allowPrivateIPs, etc.)
- Include 33 security tests (unit + integration)

Fixes #362

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 18:20:26 +01:00

169 lines
5 KiB
TypeScript

/**
* MCP integration test for security features.
* Tests that the actual tools properly reject dangerous inputs.
*/
import { queryToolHandler } from '../../src/tools/query.js';
import { parseSpec } from '../../src/lib/parser.js';
async function runTests() {
console.log('\n=== MCP Integration Security Tests ===\n');
let passed = 0;
let failed = 0;
// Test 1: ReDoS protection in query-endpoints
console.log('Test 1: query-endpoints rejects ReDoS pattern...');
try {
const result = await queryToolHandler({
path: 'test/fixtures/petstore.yaml',
pathPattern: '(a+)+$',
});
const content = result.content[0];
if (content.type === 'text' && content.text.includes('nested quantifiers')) {
console.log(' ✅ ReDoS pattern rejected');
passed++;
} else {
console.log(' ❌ ReDoS pattern NOT rejected');
console.log(' Response:', JSON.stringify(result).substring(0, 200));
failed++;
}
} catch (err) {
console.log(' ❌ Unexpected error:', (err as Error).message);
failed++;
}
// Test 2: Safe regex works
console.log('Test 2: query-endpoints accepts safe regex...');
try {
const result = await queryToolHandler({
path: 'test/fixtures/petstore.yaml',
pathPattern: '/pets.*',
});
const structured = result.structuredContent as { success: boolean };
if (structured.success) {
console.log(' ✅ Safe regex accepted');
passed++;
} else {
console.log(' ❌ Safe regex rejected unexpectedly');
failed++;
}
} catch (err) {
console.log(' ❌ Unexpected error:', (err as Error).message);
failed++;
}
// Test 3: Path traversal protection
console.log('Test 3: parseSpec rejects path traversal...');
try {
await parseSpec('../../../etc/passwd');
console.log(' ❌ Path traversal NOT blocked');
failed++;
} catch (err) {
if ((err as Error).message.includes('traversal')) {
console.log(' ✅ Path traversal blocked');
passed++;
} else {
console.log(' ❌ Wrong error:', (err as Error).message);
failed++;
}
}
// Test 4: SSRF protection - localhost
console.log('Test 4: parseSpec rejects localhost URL...');
try {
await parseSpec('http://localhost/spec.json');
console.log(' ❌ Localhost URL NOT blocked');
failed++;
} catch (err) {
if ((err as Error).message.includes('blocked')) {
console.log(' ✅ Localhost URL blocked');
passed++;
} else {
console.log(' ❌ Wrong error:', (err as Error).message);
failed++;
}
}
// Test 5: SSRF protection - private IP
console.log('Test 5: parseSpec rejects private IP...');
try {
await parseSpec('http://192.168.1.1/spec.json');
console.log(' ❌ Private IP NOT blocked');
failed++;
} catch (err) {
if ((err as Error).message.includes('Private')) {
console.log(' ✅ Private IP blocked');
passed++;
} else {
console.log(' ❌ Wrong error:', (err as Error).message);
failed++;
}
}
// Test 6: SSRF protection - file protocol
console.log('Test 6: parseSpec rejects file:// protocol...');
try {
await parseSpec('file:///etc/passwd');
console.log(' ❌ File protocol NOT blocked');
failed++;
} catch (err) {
if ((err as Error).message.includes('Protocol')) {
console.log(' ✅ File protocol blocked');
passed++;
} else {
console.log(' ❌ Wrong error:', (err as Error).message);
failed++;
}
}
// Test 7: Valid local file works
console.log('Test 7: parseSpec accepts valid local file...');
try {
const result = await parseSpec('test/fixtures/petstore.yaml');
if (result.metadata.title) {
console.log(' ✅ Valid local file parsed');
passed++;
} else {
console.log(' ❌ Parse succeeded but no title');
failed++;
}
} catch (err) {
console.log(' ❌ Unexpected error:', (err as Error).message);
failed++;
}
// Test 8: Private IPs allowed with option
console.log('Test 8: parseSpec allows private IP with allowPrivateIPs option...');
try {
await parseSpec('http://192.168.1.1/spec.json', {
security: { allowPrivateIPs: true }
});
// This will fail to connect, but that's expected - the security check should pass
console.log(' ❌ Should have thrown network error');
failed++;
} catch (err) {
const msg = (err as Error).message;
if (msg.includes('Private') || msg.includes('blocked')) {
console.log(' ❌ Private IP still blocked despite option');
failed++;
} else {
// Network error is expected (no server there)
console.log(' ✅ Security check passed (network error expected)');
passed++;
}
}
// Summary
console.log('\n=== Summary ===');
console.log(`Passed: ${passed}/${passed + failed}`);
console.log(`Failed: ${failed}/${passed + failed}`);
if (failed > 0) {
process.exit(1);
}
console.log('\n✅ All MCP integration security tests passed!\n');
}
runTests().catch(console.error);