fix(postinstall): reject dist symlink escapes

This commit is contained in:
Ayaan Zaidi
2026-04-15 11:43:16 +05:30
parent 64f258fc49
commit 2a8226f8e2
4 changed files with 65 additions and 8 deletions

View File

@@ -5,6 +5,7 @@ import { withTempDir } from "../test-helpers/temp-dir.js";
import {
collectPackageDistInventoryErrors,
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
collectPackageDistInventory,
writePackageDistInventory,
} from "./package-dist-inventory.js";
@@ -63,4 +64,17 @@ describe("package dist inventory", () => {
]);
});
});
it("rejects symlinked dist entries", async () => {
await withTempDir({ prefix: "openclaw-dist-inventory-symlink-" }, async (packageRoot) => {
const distDir = path.join(packageRoot, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(path.join(packageRoot, "escape.js"), "export {};\n", "utf8");
await fs.symlink(path.join(packageRoot, "escape.js"), path.join(distDir, "entry.js"));
await expect(collectPackageDistInventory(packageRoot)).rejects.toThrow(
"Unsafe package dist path: dist/entry.js",
);
});
});
});

View File

@@ -34,15 +34,24 @@ function isPackagedDistPath(relativePath: string): boolean {
}
async function collectRelativeFiles(rootDir: string, baseDir: string): Promise<string[]> {
try {
const rootStats = await fs.lstat(rootDir);
if (!rootStats.isDirectory() || rootStats.isSymbolicLink()) {
throw new Error(
`Unsafe package dist path: ${normalizeRelativePath(path.relative(baseDir, rootDir))}`,
);
}
const entries = await fs.readdir(rootDir, { withFileTypes: true });
const files = await Promise.all(
entries.map(async (entry) => {
const entryPath = path.join(rootDir, entry.name);
const relativePath = normalizeRelativePath(path.relative(baseDir, entryPath));
if (entry.isSymbolicLink()) {
throw new Error(`Unsafe package dist path: ${relativePath}`);
}
if (entry.isDirectory()) {
return await collectRelativeFiles(entryPath, baseDir);
}
if (entry.isFile() || entry.isSymbolicLink()) {
const relativePath = normalizeRelativePath(path.relative(baseDir, entryPath));
if (entry.isFile()) {
return isPackagedDistPath(relativePath) ? [relativePath] : [];
}
return [];