path-validation.ts
1 import path from 'path'; 2 3 /** 4 * Checks if an absolute path is within any of the allowed directories. 5 * 6 * @param absolutePath - The absolute path to check (will be normalized) 7 * @param allowedDirectories - Array of absolute allowed directory paths (will be normalized) 8 * @returns true if the path is within an allowed directory, false otherwise 9 * @throws Error if given relative paths after normalization 10 */ 11 export function isPathWithinAllowedDirectories(absolutePath: string, allowedDirectories: string[]): boolean { 12 // Type validation 13 if (typeof absolutePath !== 'string' || !Array.isArray(allowedDirectories)) { 14 return false; 15 } 16 17 // Reject empty inputs 18 if (!absolutePath || allowedDirectories.length === 0) { 19 return false; 20 } 21 22 // Reject null bytes (forbidden in paths) 23 if (absolutePath.includes('\x00')) { 24 return false; 25 } 26 27 // Normalize the input path 28 let normalizedPath: string; 29 try { 30 normalizedPath = path.resolve(path.normalize(absolutePath)); 31 } catch { 32 return false; 33 } 34 35 // Verify it's absolute after normalization 36 if (!path.isAbsolute(normalizedPath)) { 37 throw new Error('Path must be absolute after normalization'); 38 } 39 40 // Check against each allowed directory 41 return allowedDirectories.some(dir => { 42 if (typeof dir !== 'string' || !dir) { 43 return false; 44 } 45 46 // Reject null bytes in allowed dirs 47 if (dir.includes('\x00')) { 48 return false; 49 } 50 51 // Normalize the allowed directory 52 let normalizedDir: string; 53 try { 54 normalizedDir = path.resolve(path.normalize(dir)); 55 } catch { 56 return false; 57 } 58 59 // Verify allowed directory is absolute after normalization 60 if (!path.isAbsolute(normalizedDir)) { 61 throw new Error('Allowed directories must be absolute paths after normalization'); 62 } 63 64 // Check if normalizedPath is within normalizedDir 65 // Path is inside if it's the same or a subdirectory 66 if (normalizedPath === normalizedDir) { 67 return true; 68 } 69 70 // Special case for root directory to avoid double slash 71 // On Windows, we need to check if both paths are on the same drive 72 if (normalizedDir === path.sep) { 73 return normalizedPath.startsWith(path.sep); 74 } 75 76 // On Windows, also check for drive root (e.g., "C:\") 77 if (path.sep === '\\' && normalizedDir.match(/^[A-Za-z]:\\?$/)) { 78 // Ensure both paths are on the same drive 79 const dirDrive = normalizedDir.charAt(0).toLowerCase(); 80 const pathDrive = normalizedPath.charAt(0).toLowerCase(); 81 return pathDrive === dirDrive && normalizedPath.startsWith(normalizedDir.replace(/\\?$/, '\\')); 82 } 83 84 return normalizedPath.startsWith(normalizedDir + path.sep); 85 }); 86 }