Skip to main content
Glama
path-security-validator.ts12.8 kB
/** * Path Security Validator * * Provides secure file path validation and sanitization to prevent * path injection attacks, directory traversal, and other security vulnerabilities. */ import path from 'path'; import fs from 'fs/promises'; import logger from '../../../logger.js'; /** * Security validation result */ export interface PathValidationResult { /** Whether the path is valid and safe */ isValid: boolean; /** Sanitized absolute path */ sanitizedPath?: string; /** Error message if validation failed */ error?: string; /** Security warnings */ warnings?: string[]; } /** * Path security configuration */ export interface PathSecurityConfig { /** Allowed base directories */ allowedBasePaths: string[]; /** Allowed file extensions */ allowedExtensions: string[]; /** Maximum path length */ maxPathLength: number; /** Whether to allow symlinks */ allowSymlinks: boolean; /** Whether to perform strict validation */ strictMode: boolean; /** Test mode specific settings */ testMode?: { /** Additional allowed test directories */ allowedTestPaths?: string[]; /** Whether to log all test mode accesses */ enableTestLogging?: boolean; /** Maximum path length multiplier for test mode */ pathLengthMultiplier?: number; /** Whether to allow relaxed extension validation in test mode */ relaxedExtensions?: boolean; }; } /** * Default security configuration */ const DEFAULT_CONFIG: PathSecurityConfig = { allowedBasePaths: [ process.env.VIBE_CODER_OUTPUT_DIR || path.join(process.cwd(), 'VibeCoderOutput'), process.env.VIBE_TASK_MANAGER_READ_DIR || process.cwd() ], allowedExtensions: ['.md', '.json', '.txt', '.yaml', '.yml'], maxPathLength: 1000, allowSymlinks: false, strictMode: true, testMode: { allowedTestPaths: [ '/tmp', path.join(process.cwd(), '__tests__'), path.join(process.cwd(), 'test'), path.join(process.cwd(), 'tests'), path.join(process.cwd(), 'spec') ], enableTestLogging: true, pathLengthMultiplier: 2, relaxedExtensions: true } }; /** * Path Security Validator class */ export class PathSecurityValidator { private config: PathSecurityConfig; private isTestMode: boolean; constructor(config?: Partial<PathSecurityConfig>) { this.config = { ...DEFAULT_CONFIG, ...config }; this.isTestMode = process.env.NODE_ENV === 'test'; // Normalize allowed base paths this.config.allowedBasePaths = this.config.allowedBasePaths.map(basePath => path.resolve(basePath) ); if (this.isTestMode) { logger.debug('PathSecurityValidator running in test mode - security validation relaxed'); } } /** * Validate and sanitize a file path */ async validatePath(inputPath: string): Promise<PathValidationResult> { const warnings: string[] = []; try { // Basic input validation if (!inputPath || typeof inputPath !== 'string') { return { isValid: false, error: 'Path must be a non-empty string' }; } // Enhanced test mode validation - secure but permissive for testing if (this.isTestMode) { const testResult = await this.validateTestModePath(inputPath); if (!testResult.isValid) { return testResult; } // If test mode validation passes and strictMode is disabled, return early with relaxed validation if (!this.config.strictMode) { const resolvedPath = path.resolve(inputPath); return { isValid: true, sanitizedPath: resolvedPath, warnings: testResult.warnings || ['Test mode: enhanced security validation active'] }; } // Continue with normal validation if strictMode is enabled (for testing) } // Check path length if (inputPath.length > this.config.maxPathLength) { return { isValid: false, error: `Path exceeds maximum length of ${this.config.maxPathLength} characters` }; } // Check for null bytes (common in path injection attacks) if (inputPath.includes('\0')) { return { isValid: false, error: 'Path contains null bytes' }; } // Check for dangerous characters const dangerousChars = /[<>"|?*]/; const controlChars = new RegExp('[' + String.fromCharCode(0) + '-' + String.fromCharCode(31) + ']'); if (dangerousChars.test(inputPath) || controlChars.test(inputPath)) { return { isValid: false, error: 'Path contains dangerous characters' }; } // Resolve and normalize the path const resolvedPath = path.resolve(inputPath); // Check for path traversal attempts if (this.containsPathTraversal(inputPath)) { return { isValid: false, error: 'Path contains directory traversal sequences' }; } // Validate against allowed base paths const isWithinAllowedPath = this.config.allowedBasePaths.some(basePath => { const relativePath = path.relative(basePath, resolvedPath); return !relativePath.startsWith('..') && !path.isAbsolute(relativePath); }); if (!isWithinAllowedPath) { return { isValid: false, error: 'Path is outside allowed directories' }; } // Check if file exists and get stats first let stats; try { stats = await fs.lstat(resolvedPath); } catch { if (this.config.strictMode) { return { isValid: false, error: 'File does not exist or is not accessible' }; } else { warnings.push('File does not exist but path validation passed'); } } // Check for symlinks if not allowed if (stats && stats.isSymbolicLink() && !this.config.allowSymlinks) { return { isValid: false, error: 'Symbolic links are not allowed' }; } // Ensure it's a file (not a directory) - check this before extension validation if (stats && !stats.isFile()) { return { isValid: false, error: 'Path must point to a file, not a directory' }; } // Check file extension (only if we have allowed extensions configured) const extension = path.extname(resolvedPath).toLowerCase(); if (this.config.allowedExtensions.length > 0 && !this.config.allowedExtensions.includes(extension)) { return { isValid: false, error: `File extension '${extension}' is not allowed` }; } return { isValid: true, sanitizedPath: resolvedPath, warnings: warnings.length > 0 ? warnings : undefined }; } catch (error) { logger.error({ err: error, inputPath }, 'Path validation failed with exception'); return { isValid: false, error: `Path validation error: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Check if path contains traversal sequences */ private containsPathTraversal(inputPath: string): boolean { // Check for various path traversal patterns const traversalPatterns = [ '../', '..\\', '/..', '\\..', '%2e%2e%2f', '%2e%2e%5c', '..%2f', '..%5c', '%252e%252e%252f', '%252e%252e%255c' ]; const lowerPath = inputPath.toLowerCase(); return traversalPatterns.some(pattern => lowerPath.includes(pattern.toLowerCase()) ); } /** * Validate multiple paths */ async validatePaths(inputPaths: string[]): Promise<PathValidationResult[]> { return Promise.all(inputPaths.map(path => this.validatePath(path))); } /** * Update configuration */ updateConfig(newConfig: Partial<PathSecurityConfig>): void { this.config = { ...this.config, ...newConfig }; // Re-normalize allowed base paths this.config.allowedBasePaths = this.config.allowedBasePaths.map(basePath => path.resolve(basePath) ); } /** * Enhanced test mode path validation * Maintains critical security checks while allowing test operations */ private async validateTestModePath(inputPath: string): Promise<PathValidationResult> { const warnings: string[] = []; // Critical security checks that apply even in test mode - only the most dangerous patterns // 1. Check for null bytes (critical security risk) if (inputPath.includes('\0')) { return { isValid: false, error: 'Path contains null bytes - blocked even in test mode' }; } // 2. Check for extremely dangerous control characters (only most critical ones) const criticalDangerousChars = new RegExp('[' + String.fromCharCode(0) + '-' + String.fromCharCode(8) + String.fromCharCode(11) + String.fromCharCode(12) + String.fromCharCode(14) + '-' + String.fromCharCode(31) + ']'); if (criticalDangerousChars.test(inputPath)) { return { isValid: false, error: 'Path contains control characters - blocked even in test mode' }; } // 3. Only block the most critical malicious patterns in test mode // Let normal validation handle directory traversal and other patterns const criticalMaliciousPatterns = [ new RegExp(String.fromCharCode(0), 'g'), // Null bytes /\$\(/g, // Command substitution /`/g // Backticks ]; for (const pattern of criticalMaliciousPatterns) { if (pattern.test(inputPath)) { return { isValid: false, error: `Path contains potentially malicious pattern: ${pattern.source} - blocked even in test mode` }; } } // 4. Check path length (even in test mode, prevent extremely long paths) const pathLengthMultiplier = this.config.testMode?.pathLengthMultiplier ?? 2; const testModeMaxLength = this.config.maxPathLength * pathLengthMultiplier; if (inputPath.length > testModeMaxLength) { return { isValid: false, error: `Path exceeds test mode maximum length of ${testModeMaxLength} characters` }; } // 5. Test-specific allowed patterns and warnings const testPatterns = [ { pattern: /\/tmp\/.*test/i, warning: 'Test mode: allowing temporary test directory' }, { pattern: /test-output/i, warning: 'Test mode: allowing test output directory' }, { pattern: /\.test\./i, warning: 'Test mode: allowing test file pattern' }, { pattern: /mock.*data/i, warning: 'Test mode: allowing mock data access' }, { pattern: /fixtures/i, warning: 'Test mode: allowing test fixtures access' } ]; for (const { pattern, warning } of testPatterns) { if (pattern.test(inputPath)) { warnings.push(warning); break; // Only add one test pattern warning } } // 6. Log test mode access for security monitoring (if enabled) if (this.config.testMode?.enableTestLogging !== false) { logger.debug({ inputPath, testMode: true, securityLevel: 'enhanced', warnings, configuredTestPaths: this.config.testMode?.allowedTestPaths?.length || 0 }, 'Test mode path validation with enhanced security'); } return { isValid: true, warnings: warnings.length > 0 ? warnings : ['Test mode: enhanced security validation passed'] }; } /** * Get current configuration */ getConfig(): PathSecurityConfig { return { ...this.config }; } /** * Get security validation metrics for monitoring */ getSecurityMetrics(): { isTestMode: boolean; securityLevel: string; allowedBasePaths: number; allowedTestPaths: number; maxPathLength: number; testModeMaxPathLength: number; } { const pathLengthMultiplier = this.config.testMode?.pathLengthMultiplier ?? 2; return { isTestMode: this.isTestMode, securityLevel: this.isTestMode ? 'enhanced-test' : 'strict-production', allowedBasePaths: this.config.allowedBasePaths.length, allowedTestPaths: this.config.testMode?.allowedTestPaths?.length ?? 0, maxPathLength: this.config.maxPathLength, testModeMaxPathLength: this.config.maxPathLength * pathLengthMultiplier }; } } /** * Default instance for convenience */ export const defaultPathValidator = new PathSecurityValidator(); /** * Convenience function for quick path validation */ export async function validateSecurePath(inputPath: string): Promise<PathValidationResult> { return defaultPathValidator.validatePath(inputPath); }

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/freshtechbro/vibe-coder-mcp'

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