fix(security): unify root-bound write hardening

This commit is contained in:
Peter Steinberger
2026-03-02 17:11:04 +00:00
parent be3a62c5e0
commit 104d32bb64
13 changed files with 427 additions and 41 deletions

View File

@@ -1,5 +1,7 @@
import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { isPathInside } from "./path-guards.js";
export function unscopedPackageName(name: string): string {
const trimmed = name.trim();
@@ -60,3 +62,43 @@ export function resolveSafeInstallDir(params: {
}
return { ok: true, path: targetDir };
}
export async function assertCanonicalPathWithinBase(params: {
baseDir: string;
candidatePath: string;
boundaryLabel: string;
}): Promise<void> {
const baseDir = path.resolve(params.baseDir);
const candidatePath = path.resolve(params.candidatePath);
if (!isPathInside(baseDir, candidatePath)) {
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
}
const baseLstat = await fs.lstat(baseDir);
if (!baseLstat.isDirectory() || baseLstat.isSymbolicLink()) {
throw new Error(`Invalid ${params.boundaryLabel}: base directory must be a real directory`);
}
const baseRealPath = await fs.realpath(baseDir);
const validateDirectory = async (dirPath: string): Promise<void> => {
const dirLstat = await fs.lstat(dirPath);
if (!dirLstat.isDirectory() || dirLstat.isSymbolicLink()) {
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
}
const dirRealPath = await fs.realpath(dirPath);
if (!isPathInside(baseRealPath, dirRealPath)) {
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
}
};
try {
await validateDirectory(candidatePath);
return;
} catch (err) {
const code = (err as { code?: string }).code;
if (code !== "ENOENT") {
throw err;
}
}
await validateDirectory(path.dirname(candidatePath));
}