Skip to main content
Glama
security-validation.ts17.7 kB
#!/usr/bin/env ts-node /** * Security Validation Script for JWT Race Condition Fix * * This script validates that the critical JWT race condition vulnerability * has been properly fixed and that all security controls are working. */ import { JWTService } from '../src/auth/jwt-service'; import { distributedLock } from '../src/auth/distributed-lock'; import { tokenBlacklist } from '../src/auth/token-blacklist'; import { tokenRateLimiter } from '../src/auth/token-rate-limiter'; import { initializeRedis, redis } from '../src/database/redis'; import { logger } from '../src/utils/logger'; import * as crypto from 'crypto'; interface ValidationResult { testName: string; status: 'PASS' | 'FAIL' | 'SKIP'; duration: number; details: string; securityImpact: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; } interface PerformanceMetrics { operation: string; avgResponseTime: number; maxResponseTime: number; minResponseTime: number; throughput: number; errorRate: number; } class SecurityValidator { private jwtService: JWTService; private results: ValidationResult[] = []; private performanceMetrics: PerformanceMetrics[] = []; constructor() { this.jwtService = new JWTService(); } async initialize(): Promise<void> { try { await initializeRedis(); await this.jwtService.initialize(); logger.info('Security validator initialized'); } catch (error) { logger.error('Failed to initialize security validator', { error }); throw error; } } async runAllValidations(): Promise<{ results: ValidationResult[]; performanceMetrics: PerformanceMetrics[]; summary: { totalTests: number; passed: number; failed: number; criticalIssues: number; }; }> { logger.info('Starting comprehensive security validation'); const tests = [ { name: 'Race Condition Prevention', fn: this.testRaceConditionPrevention.bind(this) }, { name: 'Token Replay Attack Prevention', fn: this.testTokenReplayPrevention.bind(this) }, { name: 'Concurrent Refresh Protection', fn: this.testConcurrentRefreshProtection.bind(this) }, { name: 'Rate Limiting Effectiveness', fn: this.testRateLimitingEffectiveness.bind(this) }, { name: 'Token Blacklisting Integrity', fn: this.testTokenBlacklistingIntegrity.bind(this) }, { name: 'Distributed Lock Reliability', fn: this.testDistributedLockReliability.bind(this) }, { name: 'High Concurrency Performance', fn: this.testHighConcurrencyPerformance.bind(this) }, { name: 'Security Event Logging', fn: this.testSecurityEventLogging.bind(this) }, { name: 'Session Management', fn: this.testSessionManagement.bind(this) }, { name: 'Error Handling Security', fn: this.testErrorHandlingSecurity.bind(this) } ]; for (const test of tests) { await this.runTest(test.name, test.fn); } const summary = this.generateSummary(); logger.info('Security validation completed', summary); return { results: this.results, performanceMetrics: this.performanceMetrics, summary }; } private async runTest(name: string, testFn: () => Promise<void>): Promise<void> { const startTime = Date.now(); let status: 'PASS' | 'FAIL' | 'SKIP' = 'PASS'; let details = ''; let securityImpact: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' = 'LOW'; try { logger.info(`Running test: ${name}`); await testFn(); details = 'Test completed successfully'; } catch (error) { status = 'FAIL'; details = error.message; securityImpact = this.assessSecurityImpact(name, error); logger.error(`Test failed: ${name}`, { error: error.message }); } const duration = Date.now() - startTime; this.results.push({ testName: name, status, duration, details, securityImpact }); } private assessSecurityImpact(testName: string, error: any): 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' { if (testName.includes('Race Condition') || testName.includes('Token Replay')) { return 'CRITICAL'; } if (testName.includes('Concurrent') || testName.includes('Rate Limiting')) { return 'HIGH'; } if (testName.includes('Performance') || testName.includes('Logging')) { return 'MEDIUM'; } return 'LOW'; } private async testRaceConditionPrevention(): Promise<void> { const userId = 'test-user-race-condition'; const email = 'test@example.com'; // Generate initial token pair const tokenPair = await this.jwtService.generateTokenPair({ userId, email, roles: ['user'], permissions: ['read'], sessionId: 'test-session', mfaVerified: false }); // Mock Redis data for refresh token await redis.setex(`token_family:test-family`, 3600, JSON.stringify({ userId, sessionId: 'test-session', createdAt: new Date().toISOString(), jti: 'test-jti' })); await redis.hset(`user:${userId}`, { email, roles: JSON.stringify(['user']), permissions: JSON.stringify(['read']), mfaVerified: 'false' }); // Simulate concurrent refresh attempts const concurrentAttempts = 10; const refreshPromises = Array.from({ length: concurrentAttempts }, () => this.attemptTokenRefresh(tokenPair.refreshToken) ); const results = await Promise.allSettled(refreshPromises); const successful = results.filter(r => r.status === 'fulfilled').length; // Should have at most 1 successful refresh due to distributed locking if (successful > 1) { throw new Error(`Race condition detected: ${successful} concurrent refreshes succeeded`); } logger.info('Race condition prevention test passed', { successful, total: concurrentAttempts }); } private async attemptTokenRefresh(refreshToken: string): Promise<any> { try { return await this.jwtService.refreshAccessToken(refreshToken); } catch (error) { // Expected for most concurrent attempts throw error; } } private async testTokenReplayPrevention(): Promise<void> { const userId = 'test-user-replay'; const tokenPair = await this.jwtService.generateTokenPair({ userId, email: 'replay@example.com', roles: ['user'], permissions: ['read'], sessionId: 'replay-session', mfaVerified: false }); // First use should succeed await this.jwtService.verifyAccessToken(tokenPair.accessToken); // Revoke the token await this.jwtService.revokeToken((tokenPair.accessToken as any).jti, userId); // Replay attempt should fail try { await this.jwtService.verifyAccessToken(tokenPair.accessToken); throw new Error('Token replay attack succeeded - security violation'); } catch (error) { if (!error.message.includes('Invalid or expired')) { throw error; } } logger.info('Token replay prevention test passed'); } private async testConcurrentRefreshProtection(): Promise<void> { // Test distributed lock effectiveness const lockKey = 'test-concurrent-protection'; const concurrentLockAttempts = 20; const lockPromises = Array.from({ length: concurrentLockAttempts }, () => distributedLock.acquireLock(lockKey, { ttl: 1000, maxRetries: 1 }) ); const lockResults = await Promise.allSettled(lockPromises); const successfulLocks = lockResults.filter(r => r.status === 'fulfilled' && r.value !== null ).length; // Only one lock should succeed if (successfulLocks !== 1) { throw new Error(`Distributed lock failed: ${successfulLocks} locks acquired simultaneously`); } logger.info('Concurrent refresh protection test passed'); } private async testRateLimitingEffectiveness(): Promise<void> { const userId = 'test-user-rate-limit'; // Test validation rate limiting const validationAttempts = 150; // Should exceed limit let rateLimitedCount = 0; for (let i = 0; i < validationAttempts; i++) { try { const result = await tokenRateLimiter.checkValidationLimit(userId); if (!result.allowed) { rateLimitedCount++; } } catch (error) { rateLimitedCount++; } } // Should have significant rate limiting if (rateLimitedCount < validationAttempts * 0.3) { throw new Error(`Rate limiting ineffective: only ${rateLimitedCount} out of ${validationAttempts} were limited`); } logger.info('Rate limiting effectiveness test passed', { rateLimitedCount, total: validationAttempts }); } private async testTokenBlacklistingIntegrity(): Promise<void> { const userId = 'test-user-blacklist'; const tokenId = crypto.randomUUID(); const expiryDate = new Date(Date.now() + 3600000); // 1 hour // Add token to blacklist await tokenBlacklist.addToBlacklist(tokenId, userId, expiryDate, 'test_validation'); // Check if properly blacklisted const isBlacklisted = await tokenBlacklist.isBlacklisted(tokenId); if (!isBlacklisted) { throw new Error('Token blacklisting failed - token not found in blacklist'); } // Test user-level blacklisting const hasBlacklistedTokens = await tokenBlacklist.hasBlacklistedTokens(userId); if (!hasBlacklistedTokens) { throw new Error('User-level blacklist check failed'); } logger.info('Token blacklisting integrity test passed'); } private async testDistributedLockReliability(): Promise<void> { const lockKey = 'test-lock-reliability'; // Test basic lock acquisition and release const lock = await distributedLock.acquireLock(lockKey, { ttl: 5000 }); if (!lock) { throw new Error('Failed to acquire distributed lock'); } // Test lock extension const extended = await distributedLock.extendLock(lock, 10000); if (!extended) { throw new Error('Failed to extend distributed lock'); } // Test lock validation const isValid = await distributedLock.isLockValid(lock); if (!isValid) { throw new Error('Lock validation failed'); } // Test lock release const released = await distributedLock.releaseLock(lock); if (!released) { throw new Error('Failed to release distributed lock'); } logger.info('Distributed lock reliability test passed'); } private async testHighConcurrencyPerformance(): Promise<void> { const userId = 'test-user-performance'; const tokenPair = await this.jwtService.generateTokenPair({ userId, email: 'perf@example.com', roles: ['user'], permissions: ['read'], sessionId: 'perf-session', mfaVerified: false }); // Mock successful validation await redis.setex(`active_token:test-token`, 3600, JSON.stringify({ userId, sessionId: 'perf-session', issuedAt: new Date().toISOString() })); const concurrentValidations = 1000; const startTime = Date.now(); // Perform concurrent validations const validationPromises = Array.from({ length: concurrentValidations }, async () => { const opStart = Date.now(); try { await this.jwtService.verifyAccessToken(tokenPair.accessToken); return Date.now() - opStart; } catch (error) { return Date.now() - opStart; } }); const responseTimes = await Promise.all(validationPromises); const totalTime = Date.now() - startTime; const avgResponseTime = responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length; const maxResponseTime = Math.max(...responseTimes); const minResponseTime = Math.min(...responseTimes); const throughput = (concurrentValidations / totalTime) * 1000; // ops/sec this.performanceMetrics.push({ operation: 'token_validation_concurrent', avgResponseTime, maxResponseTime, minResponseTime, throughput, errorRate: 0 }); // Performance requirements if (avgResponseTime > 50) { throw new Error(`Performance degraded: average response time ${avgResponseTime}ms exceeds 50ms SLA`); } if (throughput < 100) { throw new Error(`Performance degraded: throughput ${throughput} ops/sec below 100 ops/sec requirement`); } logger.info('High concurrency performance test passed', { avgResponseTime, maxResponseTime, throughput }); } private async testSecurityEventLogging(): Promise<void> { // This would typically check log aggregation system // For now, we'll validate that security events are being generated const userId = 'test-user-logging'; // Generate events that should be logged await this.jwtService.generateTokenPair({ userId, email: 'logging@example.com', roles: ['user'], permissions: ['read'], sessionId: 'logging-session', mfaVerified: false }); // Simulate security violation try { await this.jwtService.verifyAccessToken('invalid-token'); } catch { // Expected } // In a real implementation, you'd check log aggregation logger.info('Security event logging test passed'); } private async testSessionManagement(): Promise<void> { const userId = 'test-user-session'; const sessionId = 'test-session-mgmt'; // Create session tokens const tokenPair = await this.jwtService.generateTokenPair({ userId, email: 'session@example.com', roles: ['user'], permissions: ['read'], sessionId, mfaVerified: false }); // Verify session exists const sessionTokens = await redis.smembers(`session_tokens:${sessionId}`); if (sessionTokens.length === 0) { throw new Error('Session tokens not properly tracked'); } // Revoke session await this.jwtService.revokeSession(sessionId); // Verify session cleanup const remainingTokens = await redis.smembers(`session_tokens:${sessionId}`); if (remainingTokens.length > 0) { throw new Error('Session tokens not properly cleaned up'); } logger.info('Session management test passed'); } private async testErrorHandlingSecurity(): Promise<void> { // Test that errors don't leak sensitive information try { await this.jwtService.verifyAccessToken('malicious.token.attempt'); } catch (error) { if (error.message.includes('secret') || error.message.includes('key')) { throw new Error('Error message leaks sensitive information'); } if (error.message !== 'Invalid or expired access token') { throw new Error('Error message format inconsistent'); } } logger.info('Error handling security test passed'); } private generateSummary() { const totalTests = this.results.length; const passed = this.results.filter(r => r.status === 'PASS').length; const failed = this.results.filter(r => r.status === 'FAIL').length; const criticalIssues = this.results.filter(r => r.status === 'FAIL' && r.securityImpact === 'CRITICAL' ).length; return { totalTests, passed, failed, criticalIssues }; } async cleanup(): Promise<void> { try { await this.jwtService.shutdown(); // Additional cleanup if needed logger.info('Security validator cleanup completed'); } catch (error) { logger.error('Error during cleanup', { error }); } } } // Main execution async function main() { const validator = new SecurityValidator(); try { await validator.initialize(); const validationResults = await validator.runAllValidations(); // Print detailed results console.log('\n=== SECURITY VALIDATION RESULTS ===\n'); validationResults.results.forEach(result => { const status = result.status === 'PASS' ? '✅' : '❌'; console.log(`${status} ${result.testName}`); console.log(` Duration: ${result.duration}ms`); console.log(` Security Impact: ${result.securityImpact}`); if (result.status === 'FAIL') { console.log(` Error: ${result.details}`); } console.log(''); }); // Print performance metrics console.log('\n=== PERFORMANCE METRICS ===\n'); validationResults.performanceMetrics.forEach(metric => { console.log(`Operation: ${metric.operation}`); console.log(` Avg Response Time: ${metric.avgResponseTime.toFixed(2)}ms`); console.log(` Max Response Time: ${metric.maxResponseTime}ms`); console.log(` Throughput: ${metric.throughput.toFixed(2)} ops/sec`); console.log(''); }); // Print summary console.log('\n=== SUMMARY ===\n'); console.log(`Total Tests: ${validationResults.summary.totalTests}`); console.log(`Passed: ${validationResults.summary.passed}`); console.log(`Failed: ${validationResults.summary.failed}`); console.log(`Critical Issues: ${validationResults.summary.criticalIssues}`); if (validationResults.summary.criticalIssues > 0) { console.log('\n❌ CRITICAL SECURITY ISSUES DETECTED - DO NOT DEPLOY'); process.exit(1); } else if (validationResults.summary.failed > 0) { console.log('\n⚠️ SOME TESTS FAILED - REVIEW BEFORE DEPLOYMENT'); process.exit(1); } else { console.log('\n✅ ALL SECURITY VALIDATIONS PASSED - SAFE TO DEPLOY'); process.exit(0); } } catch (error) { console.error('Security validation failed:', error); process.exit(1); } finally { await validator.cleanup(); } } // Run the validation if this script is executed directly if (require.main === module) { main().catch(console.error); } export { SecurityValidator, ValidationResult, PerformanceMetrics };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/perfecxion-ai/secure-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server