path-utils.ts•2.56 kB
import fs from "fs/promises";
import path from "path";
import os from "os";
/**
* Normalizes a path consistently across the application
* @param p Path to normalize
* @returns Normalized path
*/
export function normalizePath(p: string): string {
return path.normalize(p);
}
/**
* Expands a path with a tilde to use the user's home directory
* @param filepath Path that may contain a tilde
* @returns Path with tilde expanded
*/
export function expandHome(filepath: string): string {
if (filepath.startsWith("~/") || filepath === "~") {
return path.join(os.homedir(), filepath.slice(1));
}
return filepath;
}
/**
* Validates a path to ensure it is within the allowed directories
* @param requestedPath The path to validate
* @param allowedDirectories Array of allowed directory paths
* @returns The validated absolute path
* @throws Error if the path is outside allowed directories
*/
export async function validatePath(
requestedPath: string,
allowedDirectories: string[]
): Promise<string> {
const expandedPath = expandHome(requestedPath);
const absolute = path.isAbsolute(expandedPath)
? path.resolve(expandedPath)
: path.resolve(process.cwd(), expandedPath);
const normalizedRequested = normalizePath(absolute);
// Check if path is within allowed directories
const isAllowed = allowedDirectories.some(dir => normalizedRequested.startsWith(dir));
if (!isAllowed) {
throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(", ")}`);
}
// Handle symlinks by checking their real path
try {
const realPath = await fs.realpath(absolute);
const normalizedReal = normalizePath(realPath);
const isRealPathAllowed = allowedDirectories.some(dir => normalizedReal.startsWith(dir));
if (!isRealPathAllowed) {
throw new Error("Access denied - symlink target outside allowed directories");
}
return realPath;
} catch (error) {
// For new files that don't exist yet, verify parent directory
const parentDir = path.dirname(absolute);
try {
const realParentPath = await fs.realpath(parentDir);
const normalizedParent = normalizePath(realParentPath);
const isParentAllowed = allowedDirectories.some(dir => normalizedParent.startsWith(dir));
if (!isParentAllowed) {
throw new Error("Access denied - parent directory outside allowed directories");
}
return absolute;
} catch {
throw new Error(`Parent directory does not exist: ${parentDir}`);
}
}
}