codemaestroai/filesystem-mcp
highPublic repository for FileSystem MCP, Fork of @modelcontextprotocol/server-filesystem
This MCP server provides secure, restricted filesystem access to AI assistants, allowing them to read, write, edit, search, and manage files and direc...
57async function validatePath(requestedPath: string): Promise<string> {
58 const expandedPath = expandHome(requestedPath);
59 const absolute = path.isAbsolute(expandedPath)
60 ? path.resolve(expandedPath)
61 : path.resolve(process.cwd(), expandedPath);
62
63 const normalizedRequested = normalizePath(absolute);
64
65 // Check if path is within allowed directories
66 const isAllowed = allowedDirectories.some(dir => normalizedRequested.startsWith(dir));
67 if (!isAllowed) {
68 throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`);
69 }
70
71 // Handle symlinks by checking their real path
72 try {
73 const realPath = await fs.realpath(absolute);
74 const normalizedReal = normalizePath(realPath);
75 const isRealPathAllowed = allowedDirectories.some(dir => normalizedReal.startsWith(dir));
76 if (!isRealPathAllowed) {
77 throw new Error("Access denied - symlink target outside allowed directories");
78 }
79 return realPath;
80 } catch (error) {
81 // For new files that don't exist yet, verify parent directory
82 const parentDir = path.dirname(absolute);
83 try {
84 const realParentPath = await fs.realpath(parentDir);
85 const normalizedParent = normalizePath(realParentPath);
86 const isParentAllowed = allowedDirectories.some(dir => normalizedParent.startsWith(dir));
87 if (!isParentAllowed) {
88 throw new Error("Access denied - parent directory outside allowed directories");
89 }
90 return absolute;
91 } catch {
92 throw new Error(`Parent directory does not exist: ${parentDir}`);
93 }
94 }
95}// Local-only MCP, requires compromised LLM to exploit. However, if the LLM can write files (via write_file tool), it can create a malicious symlink and then trigger a race condition.
The validatePath function performs a TOCTOU (time-of-check time-of-use) race condition. It first resolves the real path of the requested path using fs.realpath, then returns that real path. However, between the check and the actual file operation (e.g., read, write), an attacker could replace a symlink within the allowed directory to point to a file outside the allowed directory. Since the returned path is the real path at check time, subsequent operations use that path, but if the symlink is swapped, the operation could access an unintended file. This is a classic symlink race condition.
ImpactAn attacker with the ability to create or modify symlinks within the allowed directories (e.g., via a compromised LLM that can write files) could trick the server into reading, writing, or moving files outside the allowed directories, bypassing the path validation.
FixRe-validate the path immediately before each file operation, or use file descriptor-based operations (e.g., open with O_NOFOLLOW) to prevent symlink races. Alternatively, open the file using the real path obtained from fs.realpath and then use the file descriptor for subsequent operations.
859case "write_file": {
860 const parsed = WriteFileArgsSchema.safeParse(args);
861 if (!parsed.success) {
862 throw new Error(`Invalid arguments for write_file: ${parsed.error}`);
863 }
864 const validPath = await validatePath(parsed.data.path);
865 await fs.writeFile(validPath, parsed.data.content, "utf-8");
866 return {
867 content: [{ type: "text", text: `Successfully wrote to ${parsed.data.path}` }],
868 };
869}859case "write_file": {
860 const parsed = WriteFileArgsSchema.safeParse(args);
861 if (!parsed.success) {
862 throw new Error(`Invalid arguments for write_file: ${parsed.error}`);
863 }
864 const validPath = await validatePath(parsed.data.path);
865 await fs.writeFile(validPath, parsed.data.content, "utf-8");
866 ...
867}
868...
869case "create_directory": {
870 const parsed = CreateDirectoryArgsSchema.safeParse(args);
871 if (!parsed.success) {
872 throw new Error(`Invalid arguments for create_directory: ${parsed.error}`);
873 }
874 const validPath = await validatePath(parsed.data.path);
875 await fs.mkdir(validPath, { recursive: true });
876 ...
877}// Local-only MCP, requires compromised LLM to exploit. Low impact.
While the path is validated to be within allowed directories, there is no validation of the file or directory name itself. An attacker could create files or directories with names containing special characters (e.g., '../', null bytes) that might cause issues in downstream processing or when the path is displayed. However, the validatePath function normalizes the path and checks against allowed directories, so path traversal via '..' is blocked. The risk is low.
ImpactMinimal. An attacker could create files with names that cause confusion or minor issues when displayed, but cannot escape the allowed directories.
FixConsider validating that the file/directory name does not contain characters that could be problematic (e.g., control characters, path separators).