From a7e6c8b650901f83c5b9dd90139e521d326c3f6e Mon Sep 17 00:00:00 2001 From: Abhinav Prakash Date: Wed, 26 Aug 2026 00:27:20 +0530 Subject: [PATCH] fix(filesystem): allow create_directory to create nested parent directories (#4629) --- src/filesystem/__tests__/lib.test.ts | 17 +++++++++++++++++ src/filesystem/index.ts | 2 +- src/filesystem/lib.ts | 27 ++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/filesystem/__tests__/lib.test.ts b/src/filesystem/__tests__/lib.test.ts index e0ae61224f..fcd0fdac73 100644 --- a/src/filesystem/__tests__/lib.test.ts +++ b/src/filesystem/__tests__/lib.test.ts @@ -205,6 +205,23 @@ describe('Lib Functions', () => { .rejects.toThrow('Parent directory does not exist'); }); + it('allows missing parent directory when allowMissingParents is true', async () => { + const allowedDir = process.platform === 'win32' ? 'C:\\Users\\test' : '/home/user'; + const deeplyNestedPath = process.platform === 'win32' ? 'C:\\Users\\test\\deep\\nested\\dir' : '/home/user/deep/nested/dir'; + + const enoentError = new Error('ENOENT') as NodeJS.ErrnoException; + enoentError.code = 'ENOENT'; + + mockFs.realpath + .mockRejectedValueOnce(enoentError) // deep/nested/dir + .mockRejectedValueOnce(enoentError) // deep/nested + .mockRejectedValueOnce(enoentError) // deep + .mockResolvedValueOnce(allowedDir); // /home/user + + const result = await validatePath(deeplyNestedPath, true); + expect(result).toBe(path.resolve(deeplyNestedPath)); + }); + it('resolves relative paths against allowed directories instead of process.cwd()', async () => { const relativePath = 'test-file.txt'; const originalCwd = process.cwd; diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 234605bb13..066408e49a 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -425,7 +425,7 @@ server.registerTool( annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: false } }, async (args: z.infer) => { - const validPath = await validatePath(args.path); + const validPath = await validatePath(args.path, true); await fs.mkdir(validPath, { recursive: true }); const text = `Successfully created directory ${args.path}`; return { diff --git a/src/filesystem/lib.ts b/src/filesystem/lib.ts index ce4af9f38a..23a0f08a75 100644 --- a/src/filesystem/lib.ts +++ b/src/filesystem/lib.ts @@ -96,7 +96,7 @@ function resolveRelativePathAgainstAllowedDirectories(relativePath: string): str } // Security & Validation Functions -export async function validatePath(requestedPath: string): Promise { +export async function validatePath(requestedPath: string, allowMissingParents: boolean = false): Promise { const expandedPath = expandHome(requestedPath); const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) @@ -123,6 +123,31 @@ export async function validatePath(requestedPath: string): Promise { // Security: For new files that don't exist yet, verify parent directory // This ensures we can't create files in unauthorized locations if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + if (allowMissingParents) { + let cur = path.dirname(absolute); + while (cur !== path.dirname(cur)) { + const normalizedCur = normalizePath(cur); + if (!isPathWithinAllowedDirectories(normalizedCur, allowedDirectories)) { + throw new Error(`Access denied - ancestor directory outside allowed directories: ${cur} not in ${allowedDirectories.join(', ')}`); + } + try { + const realCur = await fs.realpath(cur); + const normalizedRealCur = normalizePath(realCur); + if (!isPathWithinAllowedDirectories(normalizedRealCur, allowedDirectories)) { + throw new Error(`Access denied - ancestor directory outside allowed directories: ${realCur} not in ${allowedDirectories.join(', ')}`); + } + return absolute; + } catch (ancestorErr) { + if ((ancestorErr as NodeJS.ErrnoException).code === 'ENOENT') { + cur = path.dirname(cur); + continue; + } + throw ancestorErr; + } + } + return absolute; + } + const parentDir = path.dirname(absolute); try { const realParentPath = await fs.realpath(parentDir);