diff --git a/src/filesystem/__tests__/lib.test.ts b/src/filesystem/__tests__/lib.test.ts index e0ae61224f..cd830c486b 100644 --- a/src/filesystem/__tests__/lib.test.ts +++ b/src/filesystem/__tests__/lib.test.ts @@ -228,6 +228,30 @@ describe('Lib Functions', () => { process.cwd = originalCwd; } }); + + describe.skipIf(process.platform === 'win32')('Windows-style paths on POSIX hosts', () => { + it.each([ + 'C:\\Users\\me\\notes\\file.md', + 'C:/Users/me/file.md', + 'Z:\\', + 'C:' + ])('rejects %s instead of treating it as a relative path', async (windowsPath) => { + await expect(validatePath(windowsPath)) + .rejects.toThrow('Access denied - Windows-style path received on a POSIX host'); + }); + + it('rejects before touching the filesystem', async () => { + await expect(validatePath('C:\\Users\\me\\notes\\file.md')).rejects.toThrow(); + expect(mockFs.realpath).not.toHaveBeenCalled(); + }); + }); + + it('still resolves relative paths that merely contain a colon after the first character', async () => { + const colonPath = process.platform === 'win32' ? 'C:\\Users\\test\\notes\\file:C.md' : 'notes/file:C.md'; + const result = await validatePath(colonPath); + const expectedBase = process.platform === 'win32' ? 'C:\\Users\\test' : '/home/user'; + expect(result).toBe(path.resolve(expectedBase, colonPath)); + }); }); }); diff --git a/src/filesystem/lib.ts b/src/filesystem/lib.ts index ce4af9f38a..4a80eba349 100644 --- a/src/filesystem/lib.ts +++ b/src/filesystem/lib.ts @@ -97,6 +97,11 @@ function resolveRelativePathAgainstAllowedDirectories(relativePath: string): str // Security & Validation Functions export async function validatePath(requestedPath: string): Promise { + // Security: reject Windows drive-letter paths on POSIX hosts; treating them as + // relative paths would silently create literal files like "C:\Users\me\file" in the allowed root. + if (process.platform !== 'win32' && /^[A-Za-z]:(?:[\\/]|$)/.test(requestedPath)) { + throw new Error(`Access denied - Windows-style path received on a POSIX host: ${requestedPath}`); + } const expandedPath = expandHome(requestedPath); const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath)