42 lines
1.3 KiB
TypeScript
42 lines
1.3 KiB
TypeScript
import { lstat, realpath } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
const realpathOrDeepestExisting = async (target: string): Promise<string> => {
|
|
let current = target;
|
|
const tail: string[] = [];
|
|
for (;;) {
|
|
try {
|
|
const real = await realpath(current);
|
|
return path.join(real, ...tail.reverse());
|
|
} catch (error) {
|
|
if ((error as { code?: string }).code !== 'ENOENT') throw error;
|
|
const parent = path.dirname(current);
|
|
if (parent === current) throw error;
|
|
tail.push(path.basename(current));
|
|
current = parent;
|
|
}
|
|
}
|
|
};
|
|
|
|
export const assertContainedRealPath = async (
|
|
root: string,
|
|
requested: string,
|
|
opts: { forWrite?: boolean; label?: string } = {},
|
|
): Promise<string> => {
|
|
const lexical = path.resolve(root, requested);
|
|
const realRoot = await realpath(root);
|
|
const resolved = await realpathOrDeepestExisting(lexical);
|
|
if (resolved !== realRoot && !resolved.startsWith(`${realRoot}${path.sep}`)) {
|
|
throw new Error(
|
|
`Refusing to access path outside ${opts.label ?? 'root'}: ${requested}`,
|
|
);
|
|
}
|
|
if (opts.forWrite) {
|
|
const info = await lstat(lexical).catch(() => null);
|
|
if (info?.isSymbolicLink()) {
|
|
throw new Error(`Refusing to write through a symlink: ${requested}`);
|
|
}
|
|
}
|
|
return resolved;
|
|
};
|