/ src / utils / path-validation.ts
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  }