Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/filesystem/__tests__/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ server.registerTool(
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: false }
},
async (args: z.infer<typeof CreateDirectoryArgsSchema>) => {
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 {
Expand Down
27 changes: 26 additions & 1 deletion src/filesystem/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function resolveRelativePathAgainstAllowedDirectories(relativePath: string): str
}

// Security & Validation Functions
export async function validatePath(requestedPath: string): Promise<string> {
export async function validatePath(requestedPath: string, allowMissingParents: boolean = false): Promise<string> {
const expandedPath = expandHome(requestedPath);
const absolute = path.isAbsolute(expandedPath)
? path.resolve(expandedPath)
Expand All @@ -123,6 +123,31 @@ export async function validatePath(requestedPath: string): Promise<string> {
// 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);
Expand Down