64 lines
1.9 KiB
TypeScript
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;
|
|
}
|