Skip to main content
Glama
validate-path.test.ts23.4 kB
import * as path from 'path'; import { validatePath } from '../../utils/security.utils'; describe('validatePath', () => { const testRoot = '/test/root'; const windowsTestRoot = 'C:\\test\\root'; describe('Input validation', () => { it('should reject empty or invalid target paths', () => { expect(() => validatePath('', testRoot)).toThrow('Target path must be a non-empty string'); expect(() => validatePath(null as any, testRoot)).toThrow( 'Target path must be a non-empty string', ); expect(() => validatePath(undefined as any, testRoot)).toThrow( 'Target path must be a non-empty string', ); }); it('should reject empty or invalid root paths', () => { expect(() => validatePath('file.txt', '')).toThrow('Root path must be a non-empty string'); expect(() => validatePath('file.txt', null as any)).toThrow( 'Root path must be a non-empty string', ); expect(() => validatePath('file.txt', undefined as any)).toThrow( 'Root path must be a non-empty string', ); }); }); describe('Valid relative paths', () => { it('should accept simple relative paths', () => { const result = validatePath('file.txt', testRoot); expect(result).toBe(path.resolve(testRoot, 'file.txt')); }); it('should accept nested relative paths', () => { const result = validatePath('src/components/file.ts', testRoot); expect(result).toBe(path.resolve(testRoot, 'src/components/file.ts')); }); it('should accept paths with ./ prefix', () => { const result = validatePath('./src/file.ts', testRoot); expect(result).toBe(path.resolve(testRoot, 'src/file.ts')); }); it('should accept paths that resolve to root directory', () => { const result = validatePath('.', testRoot); expect(result).toBe(path.resolve(testRoot)); }); }); describe('Path traversal protection', () => { it('should reject simple parent directory traversal', () => { expect(() => validatePath('../file.txt', testRoot)).toThrow( /Path traversal attempt detected/, ); }); it('should reject deep parent directory traversal', () => { expect(() => validatePath('../../../etc/passwd', testRoot)).toThrow( /Path traversal attempt detected/, ); }); it('should reject complex traversal attempts', () => { expect(() => validatePath('src/../../etc/passwd', testRoot)).toThrow( /Path traversal attempt detected/, ); }); it('should reject mixed traversal with valid paths', () => { expect(() => validatePath('src/../../../etc/passwd', testRoot)).toThrow( /Path traversal attempt detected/, ); }); }); describe('Absolute path handling', () => { it('should accept absolute paths within root directory', () => { const absolutePathInRoot = path.resolve(testRoot, 'src/file.ts'); const result = validatePath(absolutePathInRoot, testRoot); expect(result).toBe(absolutePathInRoot); }); it('should accept root directory itself as absolute path', () => { const result = validatePath(testRoot, testRoot); expect(result).toBe(testRoot); }); it('should reject absolute paths outside root directory', () => { expect(() => validatePath('/etc/passwd', testRoot)).toThrow( /Path traversal attempt detected/, ); }); it('should reject Windows absolute paths with drive letters (malicious)', () => { expect(() => validatePath('C:\\Windows\\System32', testRoot)).toThrow( 'Absolute drive path not allowed: C:\\Windows\\System32', ); expect(() => validatePath('D:\\data\\file.txt', testRoot)).toThrow( 'Absolute drive path not allowed: D:\\data\\file.txt', ); }); it('should reject UNC paths on Windows', () => { expect(() => validatePath('\\\\server\\share\\file.txt', testRoot)).toThrow( 'UNC path not allowed: \\\\server\\share\\file.txt', ); expect(() => validatePath('\\\\localhost\\c$\\file.txt', testRoot)).toThrow( 'UNC path not allowed: \\\\localhost\\c$\\file.txt', ); }); }); describe('Windows-specific handling', () => { // Mock process.platform for Windows-specific tests const originalPlatform = process.platform; beforeEach(() => { Object.defineProperty(process, 'platform', { value: 'win32', }); }); afterEach(() => { Object.defineProperty(process, 'platform', { value: originalPlatform, }); }); describe('Path separator handling', () => { it('should handle Windows path separators correctly', () => { const result = validatePath('src\\components\\file.ts', windowsTestRoot); expect(result).toBe(path.resolve(windowsTestRoot, 'src\\components\\file.ts')); }); it('should handle mixed forward and backward slashes', () => { const result = validatePath('src/components\\services/file.ts', windowsTestRoot); // Verify the result is an absolute path containing expected components expect(path.isAbsolute(result)).toBe(true); expect(result).toContain('components'); expect(result).toContain('services'); expect(result).toContain('file.ts'); // Ensure path is properly resolved (no relative parts) expect(result).not.toContain('..'); // Verify path normalization worked (should be consistent) expect(result).toBe(validatePath('src/components\\services/file.ts', windowsTestRoot)); }); it('should handle consecutive mixed separators', () => { const result = validatePath('src\\/\\//components\\\\file.ts', windowsTestRoot); // Verify the result is an absolute path containing expected components expect(path.isAbsolute(result)).toBe(true); expect(result).toContain('components'); expect(result).toContain('file.ts'); // Ensure path is properly resolved (no relative parts) expect(result).not.toContain('..'); // Verify path normalization worked (should be consistent) expect(result).toBe(validatePath('src\\/\\//components\\\\file.ts', windowsTestRoot)); }); }); describe('Drive letter handling', () => { it('should reject absolute paths with drive letters for security', () => { const windowsDrivePath = 'C:\\project\\root'; const absolutePathInRoot = 'C:\\project\\root\\src\\file.ts'; expect(() => validatePath(absolutePathInRoot, windowsDrivePath)).toThrow( 'Absolute drive path not allowed: C:\\project\\root\\src\\file.ts', ); }); it('should reject all drive letter paths regardless of drive', () => { const cDriveRoot = 'C:\\project\\root'; const dDrivePath = 'D:\\other\\path\\file.txt'; expect(() => validatePath(dDrivePath, cDriveRoot)).toThrow( 'Absolute drive path not allowed: D:\\other\\path\\file.txt', ); }); it('should handle relative paths on Windows drives', () => { const windowsDrivePath = 'C:\\project\\root'; const result = validatePath('src\\components\\file.ts', windowsDrivePath); expect(result).toBe(path.resolve(windowsDrivePath, 'src\\components\\file.ts')); }); it('should reject malicious drive letter injection in relative paths', () => { expect(() => validatePath('C:\\malicious\\path', windowsTestRoot)).toThrow( 'Absolute drive path not allowed: C:\\malicious\\path', ); expect(() => validatePath('D:malicious\\path', windowsTestRoot)).toThrow( 'Absolute drive path not allowed: D:malicious\\path', ); }); }); describe('UNC path security', () => { it('should reject UNC paths with server names', () => { expect(() => validatePath('\\\\server\\share\\file.txt', windowsTestRoot)).toThrow( 'UNC path not allowed: \\\\server\\share\\file.txt', ); }); it('should reject localhost UNC paths', () => { expect(() => validatePath('\\\\localhost\\c$\\file.txt', windowsTestRoot)).toThrow( 'UNC path not allowed: \\\\localhost\\c$\\file.txt', ); }); it('should reject UNC paths with IP addresses', () => { expect(() => validatePath('\\\\192.168.1.1\\share\\file.txt', windowsTestRoot)).toThrow( 'UNC path not allowed: \\\\192.168.1.1\\share\\file.txt', ); }); it('should reject administrative share access attempts', () => { expect(() => validatePath('\\\\server\\c$\\windows\\system32\\file.exe', windowsTestRoot), ).toThrow('UNC path not allowed: \\\\server\\c$\\windows\\system32\\file.exe'); }); }); describe('Directory traversal with backslashes', () => { it('should reject parent directory traversal using backslashes', () => { expect(() => validatePath('..\\..\\windows\\system32\\config', windowsTestRoot)).toThrow( /Path traversal attempt detected/, ); }); it('should reject simple backslash traversal', () => { expect(() => validatePath('..\\system\\file.exe', windowsTestRoot)).toThrow( /Path traversal attempt detected/, ); }); it('should reject mixed forward and backslash traversal', () => { // Use a path that will be properly detected by path splitting expect(() => validatePath('../..\\windows/system32', windowsTestRoot)).toThrow( /Path traversal attempt detected/, ); }); it('should reject nested traversal with backslashes', () => { // Use a path that will be detected as traversal by using forward slashes for traversal expect(() => validatePath('folder/../../../system\\file.exe', windowsTestRoot)).toThrow( /Path traversal attempt detected/, ); }); it("should handle backslash paths that don't traverse", () => { // This should work as it doesn't traverse outside the root const result = validatePath('src\\components\\utils\\file.exe', windowsTestRoot); expect(result).toBe(path.resolve(windowsTestRoot, 'src\\components\\utils\\file.exe')); }); }); describe('Windows reserved names and characters', () => { it('should handle paths with Windows reserved device names', () => { // These should be allowed as filenames if properly contained within root const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM9', 'LPT1', 'LPT9']; reservedNames.forEach((name) => { const result = validatePath(`src\\${name}.txt`, windowsTestRoot); expect(result).toBe(path.resolve(windowsTestRoot, `src\\${name}.txt`)); }); }); it('should handle paths with spaces and special Windows characters', () => { const result = validatePath('Program Files\\My App\\file with spaces.txt', windowsTestRoot); expect(result).toBe( path.resolve(windowsTestRoot, 'Program Files\\My App\\file with spaces.txt'), ); }); it('should handle paths with Windows-allowed special characters', () => { const specialChars = ['$', '@', '!', '%', '&', '(', ')']; specialChars.forEach((char) => { const result = validatePath(`folder\\file${char}name.txt`, windowsTestRoot); expect(result).toBe(path.resolve(windowsTestRoot, `folder\\file${char}name.txt`)); }); }); }); describe('Windows absolute vs relative path validation', () => { it('should reject Windows absolute paths with drive letters', () => { const windowsRoot = 'C:\\project\\root'; const absolutePath = 'C:\\project\\root\\src\\components\\file.ts'; expect(() => validatePath(absolutePath, windowsRoot)).toThrow( 'Absolute drive path not allowed: C:\\project\\root\\src\\components\\file.ts', ); }); it('should accept Windows relative paths with backslashes', () => { const result = validatePath('src\\services\\api\\handler.ts', windowsTestRoot); expect(result).toBe(path.resolve(windowsTestRoot, 'src\\services\\api\\handler.ts')); }); it('should reject Windows absolute paths regardless of location', () => { const windowsRoot = 'C:\\project\\root'; const outsidePath = 'C:\\other\\project\\file.ts'; expect(() => validatePath(outsidePath, windowsRoot)).toThrow( 'Absolute drive path not allowed: C:\\other\\project\\file.ts', ); }); it('should handle Windows current directory references', () => { const result = validatePath('.\\src\\file.ts', windowsTestRoot); // Verify the result is an absolute path containing expected components expect(path.isAbsolute(result)).toBe(true); expect(result).toContain('src'); expect(result).toContain('file.ts'); // Ensure the resolved path ends correctly (cross-platform safe) expect(result.endsWith('src\\file.ts') || result.endsWith('src/file.ts')).toBe(true); }); }); describe('Windows security edge cases', () => { it('should prevent Windows path injection via alternate data streams', () => { // Alternate data streams syntax should be treated as regular filename const result = validatePath('file.txt:hidden:$DATA', windowsTestRoot); expect(result).toBe(path.resolve(windowsTestRoot, 'file.txt:hidden:$DATA')); }); it('should handle Windows long path format attempts', () => { // \\?\ prefix should be rejected as it bypasses normal path limits expect(() => validatePath('\\\\?\\C:\\very\\long\\path\\file.txt', windowsTestRoot), ).toThrow('UNC path not allowed: \\\\?\\C:\\very\\long\\path\\file.txt'); }); it('should reject device namespace paths', () => { expect(() => validatePath('\\\\.\\PhysicalDrive0', windowsTestRoot)).toThrow( 'UNC path not allowed: \\\\.\\PhysicalDrive0', ); expect(() => validatePath('\\\\.\\C:', windowsTestRoot)).toThrow( 'UNC path not allowed: \\\\.\\C:', ); }); it('should prevent Windows short name (8.3) exploitation', () => { // Short names should be handled normally by path resolution const result = validatePath('PROGRA~1\\file.txt', windowsTestRoot); expect(result).toBe(path.resolve(windowsTestRoot, 'PROGRA~1\\file.txt')); }); }); }); describe('Edge cases', () => { it('should handle paths with special characters', () => { const result = validatePath('file with spaces.txt', testRoot); expect(result).toBe(path.resolve(testRoot, 'file with spaces.txt')); }); it('should handle paths with Unicode characters', () => { const result = validatePath('файл.txt', testRoot); expect(result).toBe(path.resolve(testRoot, 'файл.txt')); }); it('should handle multiple consecutive path separators', () => { const result = validatePath('src//components///file.ts', testRoot); expect(result).toBe(path.resolve(testRoot, 'src//components///file.ts')); }); it('should handle paths ending with separators', () => { const result = validatePath('src/components/', testRoot); expect(result).toBe(path.resolve(testRoot, 'src/components/')); }); }); describe('Security edge cases', () => { it('should handle null byte injection attempts', () => { const result = validatePath('file\x00.txt', testRoot); expect(result).toBe(path.resolve(testRoot, 'file\x00.txt')); }); it('should handle paths with URL encoding', () => { const result = validatePath('file%2Etxt', testRoot); expect(result).toBe(path.resolve(testRoot, 'file%2Etxt')); }); }); describe('Path length security validation', () => { // Common system path limits for security testing const WINDOWS_MAX_PATH = 260; // Traditional Windows MAX_PATH limit const UNIX_PATH_MAX = 4096; // Common Unix PATH_MAX limit const NTFS_MAX_PATH = 32767; // Windows NTFS maximum with long path support it('should handle moderately long paths safely', () => { // Test with a path that's long but within reasonable limits const moderatePath = 'directory/'.repeat(50) + 'file.txt'; // ~550 chars const result = validatePath(moderatePath, testRoot); expect(result).toBe(path.resolve(testRoot, moderatePath)); }); it('should handle Windows MAX_PATH limit boundary', () => { // Test path exactly at Windows MAX_PATH limit (260 chars) const maxPathLength = Math.max(0, WINDOWS_MAX_PATH - testRoot.length - 1); // -1 for path separator const boundaryPath = 'a'.repeat(maxPathLength); if (maxPathLength > 0) { const result = validatePath(boundaryPath, testRoot); expect(result).toBe(path.resolve(testRoot, boundaryPath)); } }); it('should reject extremely long paths to prevent buffer overflow', () => { // Test extremely long paths that could cause buffer overflows const bufferOverflowPath = 'a'.repeat(65536) + '/file.txt'; // 64KB path // Should reject paths that exceed maximum length expect(() => validatePath(bufferOverflowPath, testRoot)).toThrow( /Path too long.*maximum allowed/, ); }); it('should handle Unix PATH_MAX boundary safely', () => { // Test path approaching Unix PATH_MAX limit but staying within component limits const shortDirName = 'dir'; const maxComponents = Math.floor(UNIX_PATH_MAX / (shortDirName.length + 1)) - 1; const longUnixPath = (shortDirName + '/').repeat(Math.min(maxComponents, 90)) + 'file.txt'; if (longUnixPath.length < UNIX_PATH_MAX) { const result = validatePath(longUnixPath, testRoot); expect(result).toBe(path.resolve(testRoot, longUnixPath)); expect(result.length).toBeLessThan(UNIX_PATH_MAX); } }); it('should reject extremely long path components to prevent DoS', () => { // Test with very long individual path components that could cause DoS // Using a path under 4096 chars total but with components over 255 chars const longComponent = 'a'.repeat(300); // Exceeds component limit const dosPath = `${longComponent}/file.txt`; // Total under 4096 // Should reject paths with components that exceed maximum length expect(() => validatePath(dosPath, testRoot)).toThrow( /Path component too long.*maximum allowed/, ); }); it('should reject deeply nested directory structures to prevent stack overflow', () => { // Test very deep nesting that could cause stack overflow const deepPath = 'a/'.repeat(200) + 'file.txt'; // 200 levels deep (exceeds MAX_DEPTH of 100) // Should reject paths that exceed maximum depth expect(() => validatePath(deepPath, testRoot)).toThrow(/Path too deep.*maximum allowed/); }); it('should validate path length limits based on resolved path', () => { // Test that the final resolved path length is considered, not just input length const shortInput = '../'.repeat(10) + 'a'.repeat(300); // This should be caught by path component length limit first (since 'a'.repeat(300) > 255) expect(() => validatePath(shortInput, testRoot)).toThrow( /Path component too long.*maximum allowed/, ); }); it('should handle Unicode characters in long paths', () => { // Test long paths with Unicode characters (which may have different byte lengths) const unicodePath = '🔒'.repeat(100) + '/файл.txt'; // Mix of emoji and Cyrillic expect(() => { const result = validatePath(unicodePath, testRoot); expect(typeof result).toBe('string'); expect(result).toContain('🔒'); expect(result).toContain('файл.txt'); }).not.toThrow(); }); it('should prevent memory exhaustion from path processing', () => { // Monitor memory usage during path validation const initialMemory = process.memoryUsage().heapUsed; // Process multiple long paths in sequence for (let i = 0; i < 100; i++) { const longPath = 'dir'.repeat(10) + `/${i}/file.txt`; // Reduced size to avoid triggering limits try { validatePath(longPath, testRoot); } catch (error) { // Expected for some cases, but shouldn't cause memory issues } } const finalMemory = process.memoryUsage().heapUsed; const memoryIncrease = finalMemory - initialMemory; // Should not consume excessive memory (more than 10MB) expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024); }); it('should enforce path length limits correctly', () => { // Test path exactly at the limit (should pass) - use many short components const shortComponent = 'a'.repeat(50); // Short components under 255 limit const numComponents = Math.floor(4096 / (shortComponent.length + 1)); // +1 for separator const maxPath = (shortComponent + '/').repeat(numComponents - 1) + 'file.txt'; if (maxPath.length <= 4096) { expect(() => validatePath(maxPath, testRoot)).not.toThrow(); } // Test path one char over the limit (should fail) const overLimitPath = 'a'.repeat(4097); // Single component over total limit expect(() => validatePath(overLimitPath, testRoot)).toThrow(/Path too long.*maximum allowed/); }); it('should enforce component length limits correctly', () => { // Test component exactly at the limit (should pass) const maxComponent = 'a'.repeat(255); expect(() => validatePath(`${maxComponent}/file.txt`, testRoot)).not.toThrow(); // Test component one char over the limit (should fail) const overLimitComponent = 'a'.repeat(256); expect(() => validatePath(`${overLimitComponent}/file.txt`, testRoot)).toThrow( /Path component too long.*maximum allowed/, ); }); it('should enforce depth limits correctly', () => { // Test path exactly at the depth limit (should pass) // The depth counts each component, so 'a/a/a/file.txt' has depth 4 const maxDepthComponents = Array(100).fill('a'); // 100 components exactly const maxDepthPath = maxDepthComponents.join('/'); expect(() => validatePath(maxDepthPath, testRoot)).not.toThrow(); // Test path one level over the limit (should fail) const overDepthComponents = Array(101).fill('a'); // 101 components const overDepthPath = overDepthComponents.join('/'); expect(() => validatePath(overDepthPath, testRoot)).toThrow(/Path too deep.*maximum allowed/); }); it('should validate resolved path length after resolution', () => { // Create a long path that, when combined with root, exceeds total limit const longPathComponents = Array(90).fill('b'.repeat(45)); // 90 components of 45 chars each const longPath = longPathComponents.join('/') + '/file.txt'; // This will be ~4050+ chars // This should fail because the total path length exceeds the limit expect(() => validatePath(longPath, testRoot)).toThrow(/Path too long.*maximum allowed/); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jakedismo/KuzuMem-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server