changemaker.lite/api/src/utils/path-validator.ts

64 lines
1.9 KiB
TypeScript

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<string> {
// 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;
}