UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

288 lines (287 loc) 12.1 kB
import fs from 'fs-extra'; import path from 'path'; import logger from '../../../logger.js'; import { getUnifiedSecurityConfig } from './unified-security-config.js'; export class PathSecurityValidator { static instance = null; config; auditEvents = []; auditCounter = 0; constructor(config) { try { const unifiedConfig = getUnifiedSecurityConfig(); const unifiedPathConfig = unifiedConfig.getPathValidatorConfig(); this.config = { allowedDirectories: unifiedPathConfig.allowedDirectories, allowedExtensions: [ '.json', '.yaml', '.yml', '.txt', '.md', '.log', '.gz', '.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte', '.py', '.java', '.go', '.rs', '.cpp', '.c', '.h', '.html', '.css', '.scss', '.sass', '.less', '.xml', '.csv', '.sql', '.sh', '.bat', '.ps1' ], blockedPatterns: [ /\.\./g, /~\//g, /\/etc\//g, /\/proc\//g, /\/sys\//g, /\/dev\//g, /\/var\/log\//g, /\/root\//g, /\/home\/[^\/]+\/\.[^\/]+/g, /\0/g, /[\x00-\x1f\x7f-\x9f]/g ], allowSymlinks: false, allowAbsolutePaths: true, maxPathLength: unifiedPathConfig.maxPathLength, ...config }; logger.info({ config: this.config, source: 'unified-security-config' }, 'Path Security Validator initialized from unified configuration'); } catch (error) { logger.warn({ err: error }, 'Unified security config not available, falling back to defaults'); this.config = { allowedDirectories: [ process.cwd(), path.join(process.cwd(), 'data'), path.join(process.cwd(), 'src'), path.join(process.cwd(), 'temp'), '/tmp', '/test' ], allowedExtensions: [ '.json', '.yaml', '.yml', '.txt', '.md', '.log', '.gz', '.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte', '.py', '.java', '.go', '.rs', '.cpp', '.c', '.h', '.html', '.css', '.scss', '.sass', '.less', '.xml', '.csv', '.sql', '.sh', '.bat', '.ps1' ], blockedPatterns: [ /\.\./g, /~\//g, /\/etc\//g, /\/proc\//g, /\/sys\//g, /\/dev\//g, /\/var\/log\//g, /\/root\//g, /\/home\/[^\/]+\/\.[^\/]+/g, /\0/g, /[\x00-\x1f\x7f-\x9f]/g ], allowSymlinks: false, allowAbsolutePaths: true, maxPathLength: 4096, ...config }; logger.info({ config: this.config, source: 'hardcoded-defaults' }, 'Path Security Validator initialized from defaults (fallback)'); } } static getInstance(config) { if (!PathSecurityValidator.instance) { PathSecurityValidator.instance = new PathSecurityValidator(config); } return PathSecurityValidator.instance; } async validatePath(filePath, _operation = 'read', context) { const startTime = Date.now(); const auditInfo = { originalPath: filePath, timestamp: new Date(), validationTime: 0 }; try { if (!filePath || typeof filePath !== 'string') { return this.createValidationResult(false, auditInfo, 'Invalid path input', 'malformed'); } if (filePath.length > this.config.maxPathLength) { return this.createValidationResult(false, auditInfo, 'Path too long', 'malformed'); } const blockedPattern = this.config.blockedPatterns.find(pattern => pattern.test(filePath)); if (blockedPattern) { return this.createValidationResult(false, auditInfo, 'Path contains blocked pattern', 'traversal'); } let canonicalPath; try { canonicalPath = path.resolve(filePath); } catch (error) { logger.debug({ error, filePath }, 'Path canonicalization failed'); return this.createValidationResult(false, auditInfo, 'Path canonicalization failed', 'malformed'); } if (this.containsTraversal(canonicalPath, filePath)) { return this.createValidationResult(false, auditInfo, 'Directory traversal detected', 'traversal'); } if (!this.isPathInWhitelist(canonicalPath)) { return this.createValidationResult(false, auditInfo, 'Path not in whitelist', 'whitelist'); } if (path.extname(canonicalPath) && !this.isExtensionAllowed(canonicalPath)) { return this.createValidationResult(false, auditInfo, 'File extension not allowed', 'whitelist'); } if (!this.config.allowSymlinks && await this.isSymbolicLink(canonicalPath)) { return this.createValidationResult(false, auditInfo, 'Symbolic links not allowed', 'symlink'); } if (path.isAbsolute(filePath) && !this.config.allowAbsolutePaths) { return this.createValidationResult(false, auditInfo, 'Absolute paths not allowed', 'absolute'); } auditInfo.validationTime = Date.now() - startTime; this.logAuditEvent('validation_success', filePath, canonicalPath, undefined, context, auditInfo.validationTime); return { valid: true, canonicalPath, securityViolation: false, auditInfo }; } catch (error) { auditInfo.validationTime = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : String(error); this.logAuditEvent('validation_failure', filePath, undefined, 'system_error', context, auditInfo.validationTime, error); return this.createValidationResult(false, auditInfo, `Validation error: ${errorMessage}`, 'malformed'); } } createValidationResult(valid, auditInfo, error, violationType, canonicalPath) { auditInfo.validationTime = Date.now() - auditInfo.timestamp.getTime(); if (!valid && violationType) { this.logAuditEvent('security_violation', auditInfo.originalPath, canonicalPath, violationType, undefined, auditInfo.validationTime); } return { valid, canonicalPath, error, securityViolation: !valid && violationType !== 'malformed', violationType, auditInfo }; } containsTraversal(canonicalPath, originalPath) { const traversalPatterns = ['../', '..\\', '%2e%2e%2f', '%2e%2e%5c']; return traversalPatterns.some(pattern => originalPath.toLowerCase().includes(pattern)); } isPathInWhitelist(canonicalPath) { return this.config.allowedDirectories.some(allowedDir => { const normalizedAllowed = path.resolve(allowedDir); return canonicalPath === normalizedAllowed || canonicalPath.startsWith(normalizedAllowed + path.sep); }); } isExtensionAllowed(filePath) { const ext = path.extname(filePath).toLowerCase(); return this.config.allowedExtensions.includes(ext); } async isSymbolicLink(filePath) { try { const stats = await fs.lstat(filePath); return stats.isSymbolicLink(); } catch { return false; } } logAuditEvent(type, originalPath, canonicalPath, violationType, context, validationTime, error) { const auditEvent = { id: `path_audit_${++this.auditCounter}_${Date.now()}`, type, originalPath, canonicalPath, violationType, userAgent: context?.userAgent, sessionId: context?.sessionId, timestamp: new Date(), validationTime: validationTime || 0, stackTrace: error instanceof Error ? error.stack : undefined }; this.auditEvents.push(auditEvent); if (this.auditEvents.length > 1000) { this.auditEvents = this.auditEvents.slice(-1000); } if (type === 'security_violation') { logger.warn({ auditEvent, originalPath, canonicalPath, violationType }, 'Path security violation detected'); } else if (type === 'validation_failure') { logger.error({ auditEvent, error: error instanceof Error ? error.message : String(error || 'Unknown error') }, 'Path validation failed'); } else { logger.debug({ auditEvent }, 'Path validation successful'); } } getAuditEvents(filter) { let events = [...this.auditEvents]; if (filter) { if (filter.type) { events = events.filter(event => event.type === filter.type); } if (filter.violationType) { events = events.filter(event => event.violationType === filter.violationType); } if (filter.since) { events = events.filter(event => event.timestamp >= filter.since); } if (filter.sessionId) { events = events.filter(event => event.sessionId === filter.sessionId); } } return events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); } getSecurityStatistics() { const total = this.auditEvents.length; const successful = this.auditEvents.filter(e => e.type === 'validation_success').length; const violations = this.auditEvents.filter(e => e.type === 'security_violation').length; const failures = this.auditEvents.filter(e => e.type === 'validation_failure').length; const avgTime = total > 0 ? this.auditEvents.reduce((sum, e) => sum + e.validationTime, 0) / total : 0; const violationsByType = {}; this.auditEvents .filter(e => e.violationType) .forEach(e => { violationsByType[e.violationType] = (violationsByType[e.violationType] || 0) + 1; }); return { totalValidations: total, successfulValidations: successful, securityViolations: violations, validationFailures: failures, averageValidationTime: avgTime, violationsByType }; } updateWhitelist(config) { this.config = { ...this.config, ...config }; logger.info({ config: this.config }, 'Path whitelist configuration updated'); } clearAuditEvents() { this.auditEvents = []; this.auditCounter = 0; logger.info('Path security audit events cleared'); } shutdown() { this.auditEvents = []; logger.info('Path Security Validator shutdown'); } } export async function validateSecurePath(filePath, operation = 'read', context) { const validator = PathSecurityValidator.getInstance(); return validator.validatePath(filePath, operation, context); } export function getPathValidator() { return PathSecurityValidator.getInstance(); }