- 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>
169 lines
5 KiB
TypeScript
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);
|