UNPKG

@gabrielmaialva33/mcp-filesystem

Version:
112 lines 4.08 kB
import fs from 'node:fs/promises'; import path from 'node:path'; import os from 'node:os'; import { AccessDeniedError, PathNotFoundError } from '../errors/index.js'; import { logger } from '../logger/index.js'; export class PathValidationCache { cache = new Map(); maxSize; ttl; constructor(maxSize = 1000, ttlMs = 60000) { this.maxSize = maxSize; this.ttl = ttlMs; } get(p) { return this.cache.get(p); } set(pat, validatedPath) { if (this.cache.size >= this.maxSize) { const oldestKey = this.cache.keys().next().value; if (oldestKey) { this.cache.delete(oldestKey); } } this.cache.set(pat, validatedPath); setTimeout(() => { this.cache.delete(pat); }, this.ttl); } clear() { this.cache.clear(); } size() { return this.cache.size; } } export const pathCache = new PathValidationCache(); export function normalizePath(p) { return path.normalize(p); } export function expandHome(filepath) { if (filepath.startsWith('~/') || filepath === '~') { return path.join(os.homedir(), filepath.slice(1)); } return filepath; } export async function validatePath(requestedPath, config) { const cachedPath = pathCache.get(requestedPath); if (cachedPath) { return cachedPath; } const expandedPath = expandHome(requestedPath); const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(process.cwd(), expandedPath); const normalizedRequested = normalizePath(absolute); const isAllowed = config.allowedDirectories.some((dir) => normalizedRequested.startsWith(dir)); if (!isAllowed) { await logger.warn(`Access denied: ${absolute}`, { allowedDirs: config.allowedDirectories, }); throw new AccessDeniedError(absolute); } try { const realPath = await fs.realpath(absolute); const normalizedReal = normalizePath(realPath); const isRealPathAllowed = config.allowedDirectories.some((dir) => normalizedReal.startsWith(dir)); if (!isRealPathAllowed) { await logger.warn(`Symlink target outside allowed directories: ${realPath}`, { original: absolute, }); throw new AccessDeniedError(absolute, 'Access denied - symlink target outside allowed directories'); } pathCache.set(requestedPath, realPath); return realPath; } catch (error) { if (error.code === 'ENOENT') { const parentDir = path.dirname(absolute); try { const realParentPath = await fs.realpath(parentDir); const normalizedParent = normalizePath(realParentPath); const isParentAllowed = config.allowedDirectories.some((dir) => normalizedParent.startsWith(dir)); if (!isParentAllowed) { await logger.warn(`Parent directory outside allowed directories: ${parentDir}`); throw new AccessDeniedError(parentDir, 'Access denied - parent directory outside allowed directories'); } pathCache.set(requestedPath, absolute); return absolute; } catch (parentError) { if (parentError.code === 'ENOENT') { await logger.warn(`Parent directory does not exist: ${parentDir}`); throw new PathNotFoundError(parentDir); } throw parentError; } } throw error; } } export async function validateFileSize(filepath, maxSize) { const stats = await fs.stat(filepath); if (stats.size > maxSize) { await logger.warn(`File size limit exceeded: ${filepath}`, { size: stats.size, maxSize, }); throw new Error(`File size exceeds limit: ${stats.size} > ${maxSize} bytes`); } return stats; } //# sourceMappingURL=path.js.map