@gabrielmaialva33/mcp-filesystem
Version:
MCP server for secure filesystem access
112 lines • 4.08 kB
JavaScript
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