Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 110 additions & 86 deletions src/filesystem/path-validation.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
Loading