Files
toolbox-portal/scripts/path-safety.mjs

94 lines
2.5 KiB
JavaScript

import { lstat, realpath, stat } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
function isMissing(error) {
return error && typeof error === 'object' && error.code === 'ENOENT';
}
export async function canonicalExistingPath(candidate, label = 'Path') {
const absolute = path.resolve(candidate);
try {
return await realpath(absolute);
} catch (error) {
throw new Error(
`${label} does not resolve to an existing path: ${absolute}`,
{
cause: error,
}
);
}
}
export async function canonicalProspectivePath(candidate) {
const absolute = path.resolve(candidate);
const suffix = [];
let current = absolute;
while (true) {
let details;
try {
details = await lstat(current);
} catch (error) {
if (!isMissing(error)) throw error;
const parent = path.dirname(current);
if (parent === current) throw error;
suffix.unshift(path.basename(current));
current = parent;
continue;
}
let canonical;
try {
canonical = await realpath(current);
} catch (error) {
if (details.isSymbolicLink() && isMissing(error)) {
throw new Error(`Path contains a dangling symbolic link: ${current}`, {
cause: error,
});
}
throw error;
}
if (suffix.length > 0) {
const canonicalDetails = await stat(canonical);
if (!canonicalDetails.isDirectory()) {
throw new Error(
`Path has a non-directory existing ancestor: ${current}`
);
}
}
return path.join(canonical, ...suffix);
}
}
export function canonicalPathsOverlap(left, right) {
const resolvedLeft = path.resolve(left);
const resolvedRight = path.resolve(right);
return (
resolvedLeft === resolvedRight ||
resolvedLeft.startsWith(`${resolvedRight}${path.sep}`) ||
resolvedRight.startsWith(`${resolvedLeft}${path.sep}`)
);
}
export async function assertSafeRemovalTarget(target) {
const canonical = await canonicalProspectivePath(target);
const root = path.parse(canonical).root;
const protectedPaths = await Promise.all([
canonicalProspectivePath(process.cwd()),
canonicalProspectivePath(os.homedir()),
]);
if (
canonical === root ||
protectedPaths.some(
(protectedPath) =>
protectedPath === canonical ||
protectedPath.startsWith(`${canonical}${path.sep}`)
)
) {
throw new Error(`Refusing unsafe output target: ${canonical}`);
}
return canonical;
}