singularity-forge/web/lib/secure-path.ts

56 lines
1.4 KiB
TypeScript
Raw Normal View History

import { existsSync, realpathSync } from "node:fs";
import { dirname, isAbsolute, relative, resolve } from "node:path";
2026-05-05 14:31:16 +02:00
function isWithinRoot(
rootRealPath: string,
candidateRealPath: string,
): boolean {
const rel = relative(rootRealPath, candidateRealPath);
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
}
/**
* Validate and resolve a requested path against the given root directory.
* Returns the resolved absolute path or null if the path is invalid.
*/
2026-05-05 14:31:16 +02:00
export function resolveSecurePath(
requestedPath: string,
root: string,
options: { mustExist?: boolean } = {},
): string | null {
if (requestedPath.startsWith("/") || requestedPath.startsWith("\\")) {
return null;
}
if (requestedPath.includes("..")) {
return null;
}
2026-05-05 14:31:16 +02:00
let rootRealPath: string;
try {
rootRealPath = realpathSync.native(root);
} catch {
return null;
}
2026-05-05 14:31:16 +02:00
const resolved = resolve(rootRealPath, requestedPath);
const rel = relative(rootRealPath, resolved);
if (rel.startsWith("..") || isAbsolute(rel)) {
return null;
}
2026-05-05 14:31:16 +02:00
try {
if (existsSync(resolved)) {
const targetRealPath = realpathSync.native(resolved);
if (!isWithinRoot(rootRealPath, targetRealPath)) return null;
} else {
if (options.mustExist) return null;
const parentRealPath = realpathSync.native(dirname(resolved));
if (!isWithinRoot(rootRealPath, parentRealPath)) return null;
}
} catch {
return null;
}
2026-05-05 14:31:16 +02:00
return resolved;
}