61 lines
2.4 KiB
TypeScript
61 lines
2.4 KiB
TypeScript
import path from "node:path";
|
|
|
|
/**
|
|
* Safely joins paths while ensuring the result stays within the base directory.
|
|
* This prevents directory traversal attacks where malicious paths like "../../etc/passwd"
|
|
* could be used to access files outside the intended directory.
|
|
*
|
|
* @param basePath The base directory that should contain the result
|
|
* @param ...paths Path segments to join with the base path
|
|
* @returns The joined path if it's within the base directory
|
|
* @throws Error if the resulting path would be outside the base directory
|
|
*/
|
|
export function safeJoin(basePath: string, ...paths: string[]): string {
|
|
// Check if any of the path segments are absolute paths (which would be unsafe)
|
|
for (const pathSegment of paths) {
|
|
if (path.isAbsolute(pathSegment)) {
|
|
throw new Error(
|
|
`Unsafe path: joining "${paths.join(", ")}" with base "${basePath}" would escape the base directory`,
|
|
);
|
|
}
|
|
// Also check for home directory shortcuts which are effectively absolute
|
|
if (pathSegment.startsWith("~/")) {
|
|
throw new Error(
|
|
`Unsafe path: joining "${paths.join(", ")}" with base "${basePath}" would escape the base directory`,
|
|
);
|
|
}
|
|
// Check for Windows-style absolute paths (C:\, D:\, etc.)
|
|
if (/^[A-Za-z]:[/\\]/.test(pathSegment)) {
|
|
throw new Error(
|
|
`Unsafe path: joining "${paths.join(", ")}" with base "${basePath}" would escape the base directory`,
|
|
);
|
|
}
|
|
// Check for UNC paths (\\server\share)
|
|
if (pathSegment.startsWith("\\\\")) {
|
|
throw new Error(
|
|
`Unsafe path: joining "${paths.join(", ")}" with base "${basePath}" would escape the base directory`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Join all the paths
|
|
const joinedPath = path.join(basePath, ...paths);
|
|
|
|
// Resolve both paths to absolute paths to handle any ".." components
|
|
const resolvedBasePath = path.resolve(basePath);
|
|
const resolvedJoinedPath = path.resolve(joinedPath);
|
|
|
|
// Check if the resolved joined path starts with the base path
|
|
// Use path.relative to ensure we're doing a proper path comparison
|
|
const relativePath = path.relative(resolvedBasePath, resolvedJoinedPath);
|
|
|
|
// If relativePath starts with ".." or is absolute, then resolvedJoinedPath is outside basePath
|
|
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
throw new Error(
|
|
`Unsafe path: joining "${paths.join(", ")}" with base "${basePath}" would escape the base directory`,
|
|
);
|
|
}
|
|
|
|
return joinedPath;
|
|
}
|