diff --git a/src/filesystem/path-validation.ts b/src/filesystem/path-validation.ts index 972e9c49d0..cff6da1cf7 100644 --- a/src/filesystem/path-validation.ts +++ b/src/filesystem/path-validation.ts @@ -1,86 +1,110 @@ -import path from 'path'; - -/** - * Checks if an absolute path is within any of the allowed directories. - * - * @param absolutePath - The absolute path to check (will be normalized) - * @param allowedDirectories - Array of absolute allowed directory paths (will be normalized) - * @returns true if the path is within an allowed directory, false otherwise - * @throws Error if given relative paths after normalization - */ -export function isPathWithinAllowedDirectories(absolutePath: string, allowedDirectories: string[]): boolean { - // Type validation - if (typeof absolutePath !== 'string' || !Array.isArray(allowedDirectories)) { - return false; - } - - // Reject empty inputs - if (!absolutePath || allowedDirectories.length === 0) { - return false; - } - - // Reject null bytes (forbidden in paths) - if (absolutePath.includes('\x00')) { - return false; - } - - // Normalize the input path - let normalizedPath: string; - try { - normalizedPath = path.resolve(path.normalize(absolutePath)); - } catch { - return false; - } - - // Verify it's absolute after normalization - if (!path.isAbsolute(normalizedPath)) { - throw new Error('Path must be absolute after normalization'); - } - - // Check against each allowed directory - return allowedDirectories.some(dir => { - if (typeof dir !== 'string' || !dir) { - return false; - } - - // Reject null bytes in allowed dirs - if (dir.includes('\x00')) { - return false; - } - - // Normalize the allowed directory - let normalizedDir: string; - try { - normalizedDir = path.resolve(path.normalize(dir)); - } catch { - return false; - } - - // Verify allowed directory is absolute after normalization - if (!path.isAbsolute(normalizedDir)) { - throw new Error('Allowed directories must be absolute paths after normalization'); - } - - // Check if normalizedPath is within normalizedDir - // Path is inside if it's the same or a subdirectory - if (normalizedPath === normalizedDir) { - return true; - } - - // Special case for root directory to avoid double slash - // On Windows, we need to check if both paths are on the same drive - if (normalizedDir === path.sep) { - return normalizedPath.startsWith(path.sep); - } - - // On Windows, also check for drive root (e.g., "C:\") - if (path.sep === '\\' && normalizedDir.match(/^[A-Za-z]:\\?$/)) { - // Ensure both paths are on the same drive - const dirDrive = normalizedDir.charAt(0).toLowerCase(); - const pathDrive = normalizedPath.charAt(0).toLowerCase(); - return pathDrive === dirDrive && normalizedPath.startsWith(normalizedDir.replace(/\\?$/, '\\')); - } - - return normalizedPath.startsWith(normalizedDir + path.sep); - }); -} +import path from 'path'; + +/** + * Checks if an absolute path is within any of the allowed directories. + * + * @param absolutePath - The absolute path to check (will be normalized) + * @param allowedDirectories - Array of absolute allowed directory paths (will be normalized) + * @returns true if the path is within an allowed directory, false otherwise + * @throws Error if given relative paths after normalization + */ +export function isPathWithinAllowedDirectories(absolutePath: string, allowedDirectories: string[]): boolean { + // Type validation + if (typeof absolutePath !== 'string' || !Array.isArray(allowedDirectories)) { + return false; + } + + // Reject empty inputs + if (!absolutePath || allowedDirectories.length === 0) { + return false; + } + + // Reject null bytes (forbidden in paths) + if (absolutePath.includes('\x00')) { + return false; + } + + // Normalize the input path + // Handle UNC paths specially to preserve the \ prefix on Windows + const isUncPath = absolutePath.startsWith('\\'); + let normalizedPath: string; + try { + if (isUncPath) { + // For UNC paths, normalize but don't resolve (resolve strips leading backslashes) + const normalized = path.normalize(absolutePath); + // path.normalize may strip one backslash from \\server\share - restore it + if (normalized.startsWith('\') && !normalized.startsWith('\\')) { + normalizedPath = '\\' + normalized.slice(2); + } else { + normalizedPath = normalized; + } + } else { + normalizedPath = path.resolve(path.normalize(absolutePath)); + } + } catch { + return false; + } + + // Verify it's absolute after normalization + if (!path.isAbsolute(normalizedPath)) { + throw new Error('Path must be absolute after normalization'); + } + + // Check against each allowed directory + return allowedDirectories.some(dir => { + if (typeof dir !== 'string' || !dir) { + return false; + } + + // Reject null bytes in allowed dirs + if (dir.includes('\x00')) { + return false; + } + + // Normalize the allowed directory + // Handle UNC paths specially to preserve the \ prefix on Windows + const isUncDir = dir.startsWith('\\'); + let normalizedDir: string; + try { + if (isUncDir) { + const normalized = path.normalize(dir); + if (normalized.startsWith('\') && !normalized.startsWith('\\')) { + normalizedDir = '\\' + normalized.slice(2); + } else { + normalizedDir = normalized; + } + } else { + normalizedDir = path.resolve(path.normalize(dir)); + } + } catch { + return false; + } + + // Verify allowed directory is absolute after normalization + if (!path.isAbsolute(normalizedDir)) { + throw new Error('Allowed directories must be absolute paths after normalization'); + } + + // Check if normalizedPath is within normalizedDir + // Path is inside if it's the same or a subdirectory + if (normalizedPath === normalizedDir) { + return true; + } + + // Special case for root directory to avoid double slash + // On Windows, we need to check if both paths are on the same drive + if (normalizedDir === path.sep) { + return normalizedPath.startsWith(path.sep); + } + + // On Windows, also check for drive root (e.g., "C:\") + if (path.sep === '\\' && normalizedDir.match(/^[A-Za-z]:\\?$/)) { + // Ensure both paths are on the same drive + const dirDrive = normalizedDir.charAt(0).toLowerCase(); + const pathDrive = normalizedPath.charAt(0).toLowerCase(); + return pathDrive === dirDrive && normalizedPath.startsWith(normalizedDir.replace(/\\?$/, '\\')); + } + + return normalizedPath.startsWith(normalizedDir + path.sep); + }); +}