import path from 'path'; import fs from 'fs/promises'; /** * Validates that a file path is safe and within the allowed base directory * Prevents path traversal attacks via: * - Null byte injection * - Directory traversal sequences (../) * - Symlink attacks * * @param basePath - The allowed base directory (must be absolute) * @param relativePath - The relative path to validate * @returns The validated absolute path * @throws Error if path validation fails */ export async function validateFilePath( basePath: string, relativePath: string ): Promise { // Check for null bytes if (relativePath.includes('\0')) { throw new Error('Invalid file path: contains null byte'); } // Resolve to absolute path const fullPath = path.resolve(basePath, relativePath); // Verify it's still within base directory const resolvedBase = path.resolve(basePath); if (!fullPath.startsWith(resolvedBase)) { throw new Error('Invalid file path: directory traversal detected'); } // Check for symlinks (resolve real path) try { const realPath = await fs.realpath(fullPath); if (!realPath.startsWith(resolvedBase)) { throw new Error('Invalid file path: symlink traversal detected'); } } catch (err: any) { if (err.code === 'ENOENT') { // File doesn't exist yet, verify parent directory const parent = path.dirname(fullPath); try { const realParent = await fs.realpath(parent); if (!realParent.startsWith(resolvedBase)) { throw new Error('Invalid file path: parent traversal detected'); } } catch (parentErr: any) { if (parentErr.code === 'ENOENT') { // Parent also doesn't exist, just verify the path is within base // This is already checked above } else { throw parentErr; } } } else { throw err; } } return fullPath; }