From ed45a8a501f35694d7808df3f85e9b5267ab6093 Mon Sep 17 00:00:00 2001 From: Will-hxw <1176843521@qq.com> Date: Tue, 21 Apr 2026 12:20:04 +0800 Subject: [PATCH] fix(filesystem): handle UNC paths on Windows in path validation UNC paths (e.g. \\192.168.1.1\share\) were failing path validation because path.resolve() strips leading backslashes on Windows. Now UNC paths are detected and handled specially - they bypass path.resolve() and are compared directly after normalization. Fixes Issue #3756 --- src/filesystem/path-validation.ts | 196 +++++++++++++++++------------- 1 file changed, 110 insertions(+), 86 deletions(-) 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); + }); +}