Skip to main content
Glama
filesystem-security.ts15.7 kB
/** * Filesystem Security Module for Vibe Task Manager * Follows Code Map Generator security patterns with system directory blacklist */ import fs from 'fs/promises'; import fsSync from 'fs'; import path from 'path'; import { getUnifiedSecurityConfig } from './unified-security-config.js'; import logger from '../../../logger.js'; /** * System directories that should be blacklisted to prevent EACCES errors */ const SYSTEM_DIRECTORY_BLACKLIST = new Set([ '/private/var/spool/postfix', '/private/var/spool/cups', '/private/var/spool/mail', '/private/var/spool/mqueue', '/private/var/db/sudo', '/private/var/db/dslocal', '/private/var/folders', '/private/var/vm', '/private/var/tmp', '/System', '/usr/bin', '/usr/sbin', '/bin', '/sbin', '/private/etc', '/private/var/root', '/private/var/log', '/Library/Application Support', '/Library/Caches', '/Library/Logs', '/Library/Preferences', '/Users/Shared', // Windows system directories 'C:\\Windows', 'C:\\Program Files', 'C:\\Program Files (x86)', 'C:\\ProgramData', 'C:\\System Volume Information', 'C:\\$Recycle.Bin', // Linux system directories '/proc', '/sys', '/dev', '/boot', '/root', '/var/log', '/var/spool', '/var/cache', '/var/lib', '/etc', '/tmp' ]); /** * File extensions that are considered safe for reading */ const SAFE_FILE_EXTENSIONS = new Set([ '.txt', '.md', '.json', '.yaml', '.yml', '.xml', '.csv', '.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte', '.py', '.java', '.go', '.rs', '.cpp', '.c', '.h', '.hpp', '.html', '.css', '.scss', '.sass', '.less', '.sql', '.sh', '.bat', '.ps1', '.dockerfile', '.gitignore', '.gitattributes', '.editorconfig', '.eslintrc', '.prettierrc', '.babelrc', '.log', '.env' ]); export interface SecurityCheckResult { allowed: boolean; reason?: string; normalizedPath?: string; securityViolation?: boolean; } export interface FilesystemSecurityConfig { enablePermissionChecking: boolean; enableBlacklist: boolean; enableExtensionFiltering: boolean; maxPathLength: number; performanceThresholdMs: number; allowedDirectories: string[]; additionalBlacklistedPaths: string[]; additionalSafeExtensions: string[]; } /** * Filesystem Security Manager */ export class FilesystemSecurity { private static instance: FilesystemSecurity; private config: FilesystemSecurityConfig; private securityMode: 'strict' | 'permissive'; private constructor(config?: Partial<FilesystemSecurityConfig>) { try { // Try to get configuration from unified security config manager const unifiedConfig = getUnifiedSecurityConfig(); const unifiedSecurityConfig = unifiedConfig.getFilesystemSecurityConfig(); this.securityMode = unifiedSecurityConfig.securityMode; this.config = { enablePermissionChecking: unifiedSecurityConfig.enablePermissionChecking, enableBlacklist: unifiedSecurityConfig.enableBlacklist, enableExtensionFiltering: unifiedSecurityConfig.enableExtensionFiltering, maxPathLength: unifiedSecurityConfig.maxPathLength, performanceThresholdMs: unifiedSecurityConfig.performanceThresholdMs, allowedDirectories: unifiedSecurityConfig.allowedDirectories, additionalBlacklistedPaths: [], additionalSafeExtensions: [], ...config }; logger.info({ securityMode: this.securityMode, config: this.config, source: 'unified-security-config' }, 'Filesystem Security initialized from unified configuration'); } catch (error) { // Fallback to environment variables if unified config is not available logger.warn({ err: error }, 'Unified security config not available, falling back to environment variables'); this.securityMode = (process.env.VIBE_TASK_MANAGER_SECURITY_MODE as 'strict' | 'permissive') || 'strict'; this.config = { enablePermissionChecking: true, enableBlacklist: true, enableExtensionFiltering: this.securityMode === 'strict', maxPathLength: 4096, performanceThresholdMs: 50, // Epic 6.2 target allowedDirectories: [ process.env.VIBE_TASK_MANAGER_READ_DIR || process.cwd(), process.env.VIBE_CODER_OUTPUT_DIR || path.join(process.cwd(), 'VibeCoderOutput') ], additionalBlacklistedPaths: [], additionalSafeExtensions: [], ...config }; logger.info({ securityMode: this.securityMode, config: this.config, source: 'environment-variables' }, 'Filesystem Security initialized from environment variables (fallback)'); } } static getInstance(config?: Partial<FilesystemSecurityConfig>): FilesystemSecurity { if (!FilesystemSecurity.instance) { FilesystemSecurity.instance = new FilesystemSecurity(config); } return FilesystemSecurity.instance; } /** * Check if a path is safe to access */ async checkPathSecurity( filePath: string, operation: 'read' | 'write' | 'execute' = 'read' ): Promise<SecurityCheckResult> { const startTime = Date.now(); try { // Basic validation if (!filePath || typeof filePath !== 'string') { return { allowed: false, reason: 'Invalid path input', securityViolation: true }; } // Check path length if (filePath.length > this.config.maxPathLength) { return { allowed: false, reason: 'Path too long', securityViolation: true }; } // Normalize the path const normalizedPath = this.normalizePath(filePath); // Check blacklist if (this.config.enableBlacklist && this.isBlacklisted(normalizedPath)) { return { allowed: false, reason: 'Path is in system directory blacklist', normalizedPath, securityViolation: true }; } // Check if path is within allowed directories if (!this.isWithinAllowedDirectories(normalizedPath)) { return { allowed: false, reason: 'Path is outside allowed directories', normalizedPath, securityViolation: true }; } // Check file extension for read operations if (operation === 'read' && this.config.enableExtensionFiltering) { const ext = path.extname(normalizedPath).toLowerCase(); if (ext && !this.isSafeExtension(ext)) { return { allowed: false, reason: 'File extension not in safe list', normalizedPath, securityViolation: false // Not a security violation, just policy }; } } // Check permissions if enabled if (this.config.enablePermissionChecking) { const permissionResult = await this.checkPermissions(normalizedPath, operation); if (!permissionResult.allowed) { return permissionResult; } } // Performance monitoring const duration = Date.now() - startTime; if (duration > this.config.performanceThresholdMs) { logger.warn({ filePath, duration, threshold: this.config.performanceThresholdMs }, 'Security check exceeded performance threshold'); } return { allowed: true, normalizedPath }; } catch (error) { logger.error({ err: error, filePath }, 'Error during security check'); return { allowed: false, reason: `Security check failed: ${error instanceof Error ? error.message : String(error)}`, securityViolation: true }; } } /** * Secure directory reading with permission checking */ async readDirSecure(dirPath: string): Promise<fsSync.Dirent[]> { const securityCheck = await this.checkPathSecurity(dirPath, 'read'); if (!securityCheck.allowed) { throw new Error(`Access denied: ${securityCheck.reason}`); } const securePath = securityCheck.normalizedPath!; try { // Check if directory exists and is readable await fs.access(securePath, fsSync.constants.R_OK); // Read directory const entries = await fs.readdir(securePath, { withFileTypes: true }); logger.debug({ path: securePath, entryCount: entries.length }, 'Directory read successfully'); return entries; } catch (error) { if (error instanceof Error && 'code' in error) { const fsError = error as { code: string }; if (fsError.code === 'ENOENT') { throw new Error(`Directory not found: ${dirPath}`); } else if (fsError.code === 'EACCES') { logger.warn({ path: securePath }, 'Permission denied for directory access'); throw new Error(`Permission denied for directory: ${dirPath}`); } } const errorMessage = error instanceof Error ? error.message : String(error); logger.error({ err: error, path: securePath }, `Error reading directory: ${errorMessage}`); throw new Error(`Could not read directory '${dirPath}': ${errorMessage}`); } } /** * Secure file stat with permission checking */ async statSecure(filePath: string): Promise<fsSync.Stats> { const securityCheck = await this.checkPathSecurity(filePath, 'read'); if (!securityCheck.allowed) { throw new Error(`Access denied: ${securityCheck.reason}`); } const securePath = securityCheck.normalizedPath!; try { const stats = await fs.stat(securePath); logger.debug({ path: securePath }, 'File stats retrieved successfully'); return stats; } catch (error) { if (error instanceof Error && 'code' in error) { const fsError = error as { code: string }; if (fsError.code === 'ENOENT') { throw new Error(`File not found: ${filePath}`); } else if (fsError.code === 'EACCES') { logger.warn({ path: securePath }, 'Permission denied for file access'); throw new Error(`Permission denied for file: ${filePath}`); } } const errorMessage = error instanceof Error ? error.message : String(error); logger.error({ err: error, path: securePath }, `Error getting file stats: ${errorMessage}`); throw new Error(`Could not get stats for '${filePath}': ${errorMessage}`); } } /** * Normalize path for consistent comparison */ private normalizePath(inputPath: string): string { try { return path.resolve(inputPath); } catch (error) { logger.warn({ inputPath, error }, 'Failed to normalize path'); return inputPath; } } /** * Check if path is blacklisted */ private isBlacklisted(normalizedPath: string): boolean { // Check system blacklist for (const blacklistedPath of SYSTEM_DIRECTORY_BLACKLIST) { if (this.isPathWithin(normalizedPath, blacklistedPath)) { return true; } } // Check additional blacklisted paths for (const blacklistedPath of this.config.additionalBlacklistedPaths) { if (this.isPathWithin(normalizedPath, blacklistedPath)) { return true; } } return false; } /** * Check if path is within allowed directories */ private isWithinAllowedDirectories(normalizedPath: string): boolean { for (const allowedDir of this.config.allowedDirectories) { if (this.isPathWithin(normalizedPath, allowedDir)) { return true; } } return false; } /** * Check if child path is within parent path */ private isPathWithin(childPath: string, parentPath: string): boolean { const normalizedChild = path.resolve(childPath); const normalizedParent = path.resolve(parentPath); // Check for exact match if (normalizedChild === normalizedParent) { return true; } // Ensure parent path ends with separator for proper prefix matching const parentWithSep = normalizedParent.endsWith(path.sep) ? normalizedParent : normalizedParent + path.sep; return normalizedChild.startsWith(parentWithSep); } /** * Check if file extension is safe */ private isSafeExtension(extension: string): boolean { return SAFE_FILE_EXTENSIONS.has(extension.toLowerCase()) || this.config.additionalSafeExtensions.includes(extension.toLowerCase()); } /** * Check file/directory permissions */ private async checkPermissions( normalizedPath: string, operation: 'read' | 'write' | 'execute' ): Promise<SecurityCheckResult> { try { let accessMode: number; switch (operation) { case 'read': accessMode = fsSync.constants.R_OK; break; case 'write': accessMode = fsSync.constants.W_OK; break; case 'execute': accessMode = fsSync.constants.X_OK; break; default: accessMode = fsSync.constants.F_OK; } await fs.access(normalizedPath, accessMode); return { allowed: true, normalizedPath }; } catch (error) { if (error instanceof Error && 'code' in error) { const fsError = error as { code: string }; if (fsError.code === 'ENOENT') { return { allowed: false, reason: 'Path does not exist', normalizedPath, securityViolation: false }; } else if (fsError.code === 'EACCES') { return { allowed: false, reason: `Permission denied for ${operation} operation`, normalizedPath, securityViolation: false }; } } return { allowed: false, reason: `Permission check failed: ${error instanceof Error ? error.message : String(error)}`, normalizedPath, securityViolation: true }; } } /** * Update security configuration */ updateConfig(newConfig: Partial<FilesystemSecurityConfig>): void { this.config = { ...this.config, ...newConfig }; logger.info({ config: this.config }, 'Filesystem security configuration updated'); } /** * Get current security configuration */ getConfig(): FilesystemSecurityConfig { return { ...this.config }; } /** * Get security mode */ getSecurityMode(): 'strict' | 'permissive' { return this.securityMode; } /** * Get security statistics */ getSecurityStats(): { securityMode: string; blacklistedPathsCount: number; allowedDirectoriesCount: number; safeExtensionsCount: number; } { return { securityMode: this.securityMode, blacklistedPathsCount: SYSTEM_DIRECTORY_BLACKLIST.size + this.config.additionalBlacklistedPaths.length, allowedDirectoriesCount: this.config.allowedDirectories.length, safeExtensionsCount: SAFE_FILE_EXTENSIONS.size + this.config.additionalSafeExtensions.length }; } /** * Add a path to the blacklist */ addToBlacklist(pathToBlock: string): void { const normalizedPath = path.resolve(pathToBlock); if (!this.config.additionalBlacklistedPaths.includes(normalizedPath)) { this.config.additionalBlacklistedPaths.push(normalizedPath); logger.info({ path: normalizedPath }, 'Path added to blacklist'); } } /** * Remove a path from the blacklist */ removeFromBlacklist(pathToUnblock: string): void { const normalizedPath = path.resolve(pathToUnblock); const index = this.config.additionalBlacklistedPaths.indexOf(normalizedPath); if (index !== -1) { this.config.additionalBlacklistedPaths.splice(index, 1); logger.info({ path: normalizedPath }, 'Path removed from blacklist'); } } }

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