vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
370 lines (369 loc) • 14.5 kB
JavaScript
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';
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',
'C:\\Windows',
'C:\\Program Files',
'C:\\Program Files (x86)',
'C:\\ProgramData',
'C:\\System Volume Information',
'C:\\$Recycle.Bin',
'/proc',
'/sys',
'/dev',
'/boot',
'/root',
'/var/log',
'/var/spool',
'/var/cache',
'/var/lib',
'/etc',
'/tmp'
]);
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 class FilesystemSecurity {
static instance;
config;
securityMode;
constructor(config) {
try {
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) {
logger.warn({ err: error }, 'Unified security config not available, falling back to environment variables');
this.securityMode = process.env.VIBE_TASK_MANAGER_SECURITY_MODE || 'strict';
this.config = {
enablePermissionChecking: true,
enableBlacklist: true,
enableExtensionFiltering: this.securityMode === 'strict',
maxPathLength: 4096,
performanceThresholdMs: 50,
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) {
if (!FilesystemSecurity.instance) {
FilesystemSecurity.instance = new FilesystemSecurity(config);
}
return FilesystemSecurity.instance;
}
async checkPathSecurity(filePath, operation = 'read') {
const startTime = Date.now();
try {
if (!filePath || typeof filePath !== 'string') {
return {
allowed: false,
reason: 'Invalid path input',
securityViolation: true
};
}
if (filePath.length > this.config.maxPathLength) {
return {
allowed: false,
reason: 'Path too long',
securityViolation: true
};
}
const normalizedPath = this.normalizePath(filePath);
if (this.config.enableBlacklist && this.isBlacklisted(normalizedPath)) {
return {
allowed: false,
reason: 'Path is in system directory blacklist',
normalizedPath,
securityViolation: true
};
}
if (!this.isWithinAllowedDirectories(normalizedPath)) {
return {
allowed: false,
reason: 'Path is outside allowed directories',
normalizedPath,
securityViolation: true
};
}
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
};
}
}
if (this.config.enablePermissionChecking) {
const permissionResult = await this.checkPermissions(normalizedPath, operation);
if (!permissionResult.allowed) {
return permissionResult;
}
}
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
};
}
}
async readDirSecure(dirPath) {
const securityCheck = await this.checkPathSecurity(dirPath, 'read');
if (!securityCheck.allowed) {
throw new Error(`Access denied: ${securityCheck.reason}`);
}
const securePath = securityCheck.normalizedPath;
try {
await fs.access(securePath, fsSync.constants.R_OK);
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;
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}`);
}
}
async statSecure(filePath) {
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;
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}`);
}
}
normalizePath(inputPath) {
try {
return path.resolve(inputPath);
}
catch (error) {
logger.warn({ inputPath, error }, 'Failed to normalize path');
return inputPath;
}
}
isBlacklisted(normalizedPath) {
for (const blacklistedPath of SYSTEM_DIRECTORY_BLACKLIST) {
if (this.isPathWithin(normalizedPath, blacklistedPath)) {
return true;
}
}
for (const blacklistedPath of this.config.additionalBlacklistedPaths) {
if (this.isPathWithin(normalizedPath, blacklistedPath)) {
return true;
}
}
return false;
}
isWithinAllowedDirectories(normalizedPath) {
for (const allowedDir of this.config.allowedDirectories) {
if (this.isPathWithin(normalizedPath, allowedDir)) {
return true;
}
}
return false;
}
isPathWithin(childPath, parentPath) {
const normalizedChild = path.resolve(childPath);
const normalizedParent = path.resolve(parentPath);
if (normalizedChild === normalizedParent) {
return true;
}
const parentWithSep = normalizedParent.endsWith(path.sep)
? normalizedParent
: normalizedParent + path.sep;
return normalizedChild.startsWith(parentWithSep);
}
isSafeExtension(extension) {
return SAFE_FILE_EXTENSIONS.has(extension.toLowerCase()) ||
this.config.additionalSafeExtensions.includes(extension.toLowerCase());
}
async checkPermissions(normalizedPath, operation) {
try {
let accessMode;
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;
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
};
}
}
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
logger.info({ config: this.config }, 'Filesystem security configuration updated');
}
getConfig() {
return { ...this.config };
}
getSecurityMode() {
return this.securityMode;
}
getSecurityStats() {
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
};
}
addToBlacklist(pathToBlock) {
const normalizedPath = path.resolve(pathToBlock);
if (!this.config.additionalBlacklistedPaths.includes(normalizedPath)) {
this.config.additionalBlacklistedPaths.push(normalizedPath);
logger.info({ path: normalizedPath }, 'Path added to blacklist');
}
}
removeFromBlacklist(pathToUnblock) {
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');
}
}
}