mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor: clarify archive staging intent
This commit is contained in:
@@ -10,6 +10,7 @@ import { extractArchive, resolveArchiveKind, resolvePackedRootDir } from "./arch
|
||||
|
||||
let fixtureRoot = "";
|
||||
let fixtureCount = 0;
|
||||
const directorySymlinkType = process.platform === "win32" ? "junction" : undefined;
|
||||
|
||||
async function makeTempDir(prefix = "case") {
|
||||
const dir = path.join(fixtureRoot, `${prefix}-${fixtureCount++}`);
|
||||
@@ -48,6 +49,14 @@ async function writePackageArchive(params: {
|
||||
await tar.c({ cwd: params.workDir, file: params.archivePath }, ["package"]);
|
||||
}
|
||||
|
||||
async function createDirectorySymlink(targetDir: string, linkPath: string) {
|
||||
await fs.symlink(targetDir, linkPath, directorySymlinkType);
|
||||
}
|
||||
|
||||
async function expectPathMissing(filePath: string) {
|
||||
await expect(fs.stat(filePath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
}
|
||||
|
||||
async function expectExtractedSizeBudgetExceeded(params: {
|
||||
archivePath: string;
|
||||
destDir: string;
|
||||
@@ -119,11 +128,7 @@ describe("archive utils", () => {
|
||||
content: "hi",
|
||||
});
|
||||
await fs.rm(extractDir, { recursive: true, force: true });
|
||||
await fs.symlink(
|
||||
realExtractDir,
|
||||
extractDir,
|
||||
process.platform === "win32" ? "junction" : undefined,
|
||||
);
|
||||
await createDirectorySymlink(realExtractDir, extractDir);
|
||||
|
||||
await expect(
|
||||
extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 }),
|
||||
@@ -131,9 +136,7 @@ describe("archive utils", () => {
|
||||
code: "destination-symlink",
|
||||
} satisfies Partial<ArchiveSecurityError>);
|
||||
|
||||
await expect(
|
||||
fs.stat(path.join(realExtractDir, "package", "hello.txt")),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expectPathMissing(path.join(realExtractDir, "package", "hello.txt"));
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -154,13 +157,7 @@ describe("archive utils", () => {
|
||||
await withArchiveCase("zip", async ({ workDir, archivePath, extractDir }) => {
|
||||
const outsideDir = path.join(workDir, "outside");
|
||||
await fs.mkdir(outsideDir, { recursive: true });
|
||||
// Use 'junction' on Windows — junctions target directories without
|
||||
// requiring SeCreateSymbolicLinkPrivilege.
|
||||
await fs.symlink(
|
||||
outsideDir,
|
||||
path.join(extractDir, "escape"),
|
||||
process.platform === "win32" ? "junction" : undefined,
|
||||
);
|
||||
await createDirectorySymlink(outsideDir, path.join(extractDir, "escape"));
|
||||
|
||||
const zip = new JSZip();
|
||||
zip.file("escape/pwn.txt", "owned");
|
||||
@@ -273,11 +270,7 @@ describe("archive utils", () => {
|
||||
await fs.mkdir(outsideDir, { recursive: true });
|
||||
await fs.mkdir(path.join(archiveRoot, "escape"), { recursive: true });
|
||||
await fs.writeFile(path.join(archiveRoot, "escape", "pwn.txt"), "owned");
|
||||
await fs.symlink(
|
||||
outsideDir,
|
||||
path.join(extractDir, "escape"),
|
||||
process.platform === "win32" ? "junction" : undefined,
|
||||
);
|
||||
await createDirectorySymlink(outsideDir, path.join(extractDir, "escape"));
|
||||
await tar.c({ cwd: archiveRoot, file: archivePath }, ["escape"]);
|
||||
|
||||
await expect(
|
||||
@@ -286,9 +279,7 @@ describe("archive utils", () => {
|
||||
code: "destination-symlink-traversal",
|
||||
} satisfies Partial<ArchiveSecurityError>);
|
||||
|
||||
await expect(fs.stat(path.join(outsideDir, "pwn.txt"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await expectPathMissing(path.join(outsideDir, "pwn.txt"));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -575,6 +575,10 @@ export async function extractArchive(params: {
|
||||
stripComponents: params.stripComponents,
|
||||
limits,
|
||||
});
|
||||
// A canonical cwd is not enough here: tar can still follow
|
||||
// pre-existing child symlinks in the live destination tree.
|
||||
// Extract into a private staging dir first, then merge through
|
||||
// the same safe-open boundary checks used by direct file writes.
|
||||
await tar.x({
|
||||
file: params.archivePath,
|
||||
cwd: stagingDir,
|
||||
|
||||
Reference in New Issue
Block a user