Skip to content
Open
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
24 changes: 24 additions & 0 deletions src/filesystem/__tests__/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
});
});

Expand Down
5 changes: 5 additions & 0 deletions src/filesystem/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ function resolveRelativePathAgainstAllowedDirectories(relativePath: string): str

// Security & Validation Functions
export async function validatePath(requestedPath: string): Promise<string> {
// 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)
Expand Down