#!/usr/bin/env node
/**
* Security Features Test Suite
* Tests rate limiting, input sanitization, and secrets management
*/
import { RateLimiter } from '../src/utils/rate-limiter.js';
import { InputSanitizer } from '../src/utils/input-sanitizer.js';
import { SecretsManager } from '../src/utils/secrets-manager.js';
import assert from 'assert';
// ANSI colors for output
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m'
};
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`);
}
// Test Rate Limiting
async function testRateLimiting() {
log('\nš Testing Rate Limiting', 'cyan');
log('-'.repeat(50), 'cyan');
const limiter = new RateLimiter({
tokensPerInterval: 5,
interval: 1000, // 1 second
maxBurst: 10,
enableLogging: false
});
const clientId = 'test-client';
let passed = 0;
let failed = 0;
// Test 1: Normal requests within limit
log('Test 1: Normal requests within limit');
for (let i = 0; i < 5; i++) {
const result = await limiter.checkLimit(clientId, 1);
if (result.allowed) {
passed++;
} else {
failed++;
}
}
assert.equal(passed, 5, 'Should allow 5 requests');
log(' ā
Allowed 5 requests within limit', 'green');
// Test 2: Burst capacity
log('Test 2: Burst capacity');
passed = 0;
for (let i = 0; i < 5; i++) {
const result = await limiter.checkLimit(clientId, 1);
if (result.allowed) {
passed++;
}
}
assert.equal(passed, 5, 'Should allow burst up to 10 total');
log(' ā
Burst capacity working', 'green');
// Test 3: Rate limit exceeded
log('Test 3: Rate limit exceeded');
const result = await limiter.checkLimit(clientId, 1);
assert.equal(result.allowed, false, 'Should block after limit');
assert(result.retryAfter > 0, 'Should provide retry time');
log(` ā
Rate limit enforced, retry after ${result.retryAfter}ms`, 'green');
// Test 4: Token refill
log('Test 4: Token refill after wait');
await new Promise(resolve => setTimeout(resolve, 1100));
const refreshResult = await limiter.checkLimit(clientId, 1);
assert.equal(refreshResult.allowed, true, 'Should allow after refill');
log(' ā
Tokens refilled after interval', 'green');
// Clean up
limiter.destroy();
log('ā
Rate limiting tests passed!', 'green');
return true;
}
// Test Input Sanitization
async function testInputSanitization() {
log('\nš Testing Input Sanitization', 'cyan');
log('-'.repeat(50), 'cyan');
const sanitizer = new InputSanitizer({
enableLogging: false
});
// Test 1: Path sanitization - directory traversal
log('Test 1: Directory traversal prevention');
const maliciousPath = '../../../etc/passwd';
const pathResult = sanitizer.sanitizePath(maliciousPath);
assert.equal(pathResult.safe, false, 'Should reject directory traversal');
assert(pathResult.errors.length > 0, 'Should provide error messages');
log(' ā
Directory traversal blocked', 'green');
// Test 2: Valid path sanitization
log('Test 2: Valid path sanitization');
const validPath = 'output/image.png';
const validResult = sanitizer.sanitizePath(validPath);
assert.equal(validResult.safe, true, 'Should accept valid path');
assert.equal(validResult.sanitized, 'output/image.png');
log(' ā
Valid path accepted', 'green');
// Test 3: Filename sanitization
log('Test 3: Filename sanitization');
const dangerousFilename = '../../evil<script>.png';
const filenameResult = sanitizer.sanitizeFilename(dangerousFilename);
assert.equal(filenameResult.safe, true, 'Should sanitize filename');
assert(!filenameResult.sanitized.includes('..'), 'Should remove traversal');
assert(!filenameResult.sanitized.includes('<'), 'Should remove special chars');
log(` ā
Filename sanitized: ${filenameResult.sanitized}`, 'green');
// Test 4: Prompt sanitization
log('Test 4: Prompt sanitization');
const xssPrompt = 'Generate <script>alert("XSS")</script> image';
const promptResult = sanitizer.sanitizePrompt(xssPrompt);
assert.equal(promptResult.safe, true, 'Should sanitize prompt');
assert(!promptResult.sanitized.includes('<script>'), 'Should escape HTML');
log(' ā
XSS attempt sanitized', 'green');
// Test 5: Number sanitization
log('Test 5: Number sanitization');
const numberResult = sanitizer.sanitizeNumber('999999', { min: 1, max: 100 });
assert.equal(numberResult.sanitized, 100, 'Should clamp to max');
const negativeResult = sanitizer.sanitizeNumber(-50, { allowNegative: false });
assert.equal(negativeResult.sanitized, 50, 'Should handle negative numbers');
log(' ā
Numbers properly constrained', 'green');
// Test 6: Complete request sanitization
log('Test 6: Complete request sanitization');
const request = {
prompt: 'A beautiful <b>landscape</b>',
width: 9999,
height: -100,
steps: 0,
seed: 42
};
const requestResult = sanitizer.sanitizeRequest('generate_image', request);
assert.equal(requestResult.valid, true, 'Should sanitize request');
assert.equal(requestResult.sanitized.width, 2048, 'Should limit width');
assert.equal(requestResult.sanitized.height, 64, 'Should fix negative height');
assert.equal(requestResult.sanitized.steps, 1, 'Should fix zero steps');
log(' ā
Request parameters sanitized', 'green');
log('ā
Input sanitization tests passed!', 'green');
return true;
}
// Test Secrets Management
async function testSecretsManagement() {
log('\nš Testing Secrets Management', 'cyan');
log('-'.repeat(50), 'cyan');
// Use mock secrets for testing
const secretsManager = new SecretsManager({
secretsPath: '/non-existent-for-testing',
fallbackToEnv: true,
enableLogging: false,
enableCaching: true
});
// Test 1: Environment variable fallback
log('Test 1: Environment variable fallback');
process.env.TEST_SECRET = 'test-value-123';
const secret = await secretsManager.loadSecret('TEST_SECRET');
assert.equal(secret, 'test-value-123', 'Should load from environment');
log(' ā
Loaded secret from environment', 'green');
// Test 2: Caching
log('Test 2: Secret caching');
const cachedSecret = await secretsManager.loadSecret('TEST_SECRET');
assert.equal(cachedSecret, 'test-value-123', 'Should return cached value');
log(' ā
Secret cached successfully', 'green');
// Test 3: Safe comparison
log('Test 3: Safe secret comparison');
const match = SecretsManager.safeCompare('secret123', 'secret123');
const noMatch = SecretsManager.safeCompare('secret123', 'secret456');
assert.equal(match, true, 'Should match identical secrets');
assert.equal(noMatch, false, 'Should not match different secrets');
log(' ā
Safe comparison working', 'green');
// Test 4: Secret masking
log('Test 4: Secret masking for logs');
const masked = SecretsManager.maskSecret('sk-ant-api03-verylongsecret');
assert(masked.includes('*'), 'Should contain mask characters');
assert(!masked.includes('verylongsecret'), 'Should not expose full secret');
log(` ā
Secret masked: ${masked}`, 'green');
// Test 5: Required secrets validation
log('Test 5: Required secrets validation');
process.env.REQUIRED_SECRET = 'present';
const validation = await secretsManager.validateSecrets(['REQUIRED_SECRET', 'MISSING_SECRET']);
assert.equal(validation.valid, false, 'Should detect missing secrets');
assert(validation.missing.includes('MISSING_SECRET'), 'Should list missing secrets');
log(' ā
Missing secrets detected', 'green');
// Clean up
delete process.env.TEST_SECRET;
delete process.env.REQUIRED_SECRET;
secretsManager.clearCache();
log('ā
Secrets management tests passed!', 'green');
return true;
}
// Test Integration
async function testSecurityIntegration() {
log('\nš Testing Security Integration', 'cyan');
log('-'.repeat(50), 'cyan');
// Simulate a request flow with all security features
const limiter = new RateLimiter({ tokensPerInterval: 10, interval: 1000 });
const sanitizer = new InputSanitizer();
const secretsManager = new SecretsManager({ fallbackToEnv: true });
// Mock request
const clientId = 'integration-test';
const request = {
operation: 'generate_image',
params: {
prompt: 'Test prompt with <script>evil</script>',
width: 5000,
height: 5000,
image_path: '../../../etc/passwd'
}
};
log('Simulating secure request flow...');
// Step 1: Rate limiting
const rateCheck = await limiter.checkLimit(clientId, 5);
if (!rateCheck.allowed) {
log(' ā Rate limit exceeded', 'red');
return false;
}
log(' ā
Rate limit check passed', 'green');
// Step 2: Input sanitization
const sanitized = sanitizer.sanitizeRequest(request.operation, request.params);
if (!sanitized.valid && request.params.image_path) {
log(' ā
Malicious path blocked', 'green');
}
log(` ā
Inputs sanitized: width=${sanitized.sanitized.width}`, 'green');
// Step 3: Authentication check (mock)
process.env.MCP_AUTH_TOKEN = 'test-auth-token';
const authToken = await secretsManager.loadSecret('MCP_AUTH_TOKEN');
if (authToken) {
log(' ā
Authentication token loaded', 'green');
}
// Clean up
limiter.destroy();
delete process.env.MCP_AUTH_TOKEN;
log('ā
Security integration test passed!', 'green');
return true;
}
// Run all tests
async function runAllTests() {
log('š Security Features Test Suite', 'cyan');
log('=' .repeat(50), 'cyan');
const tests = [
{ name: 'Rate Limiting', fn: testRateLimiting },
{ name: 'Input Sanitization', fn: testInputSanitization },
{ name: 'Secrets Management', fn: testSecretsManagement },
{ name: 'Security Integration', fn: testSecurityIntegration }
];
const results = [];
for (const test of tests) {
try {
const passed = await test.fn();
results.push({ name: test.name, passed });
} catch (error) {
log(`ā Test "${test.name}" failed: ${error.message}`, 'red');
console.error(error.stack);
results.push({ name: test.name, passed: false });
}
}
// Summary
log('\n' + '=' .repeat(50), 'cyan');
log('š Test Summary', 'cyan');
log('=' .repeat(50), 'cyan');
let passedCount = 0;
for (const result of results) {
const icon = result.passed ? 'ā
' : 'ā';
const color = result.passed ? 'green' : 'red';
log(`${icon} ${result.name}`, color);
if (result.passed) passedCount++;
}
const allPassed = passedCount === results.length;
log('\n' + '=' .repeat(50), 'cyan');
log(`Results: ${passedCount}/${results.length} tests passed`, allPassed ? 'green' : 'yellow');
if (allPassed) {
log('š All security tests passed!', 'green');
log('\nš Your MCP server is secure with:', 'green');
log(' ⢠Rate limiting to prevent abuse', 'green');
log(' ⢠Input sanitization to block attacks', 'green');
log(' ⢠Secrets management for sensitive data', 'green');
} else {
log('ā ļø Some tests failed. Please review security configuration.', 'yellow');
}
process.exit(allPassed ? 0 : 1);
}
// Run tests
runAllTests().catch(error => {
log(`Fatal error: ${error.message}`, 'red');
console.error(error.stack);
process.exit(1);
});