feat: release 2.7.0-rc2
This commit is contained in:
@@ -160,6 +160,158 @@ describe("source materialization", () => {
|
||||
expect(await readlink(join(destination, "run"))).toBe("bin/run");
|
||||
});
|
||||
|
||||
test("rejects effective symlink escapes in either manifest order before writing or reading file blobs", async () => {
|
||||
for (const entries of [
|
||||
[
|
||||
["sub", "."],
|
||||
["chain", "sub/.."],
|
||||
],
|
||||
[
|
||||
["chain", "sub/.."],
|
||||
["sub", "."],
|
||||
],
|
||||
]) {
|
||||
const root = await mkdtemp(join(tmpdir(), "kuber-materialize-"));
|
||||
roots.push(root);
|
||||
const destination = join(root, "workspace");
|
||||
const file = Buffer.from("content");
|
||||
const fileDigest = digest(file);
|
||||
const blobs = new Map<Sha256Digest, Uint8Array>();
|
||||
const manifest: WorkspaceManifest = {
|
||||
version: BUILD_PROTOCOL_VERSION,
|
||||
files: [
|
||||
{
|
||||
path: "file",
|
||||
type: "file",
|
||||
digest: fileDigest,
|
||||
size: file.length,
|
||||
mode: 0o644,
|
||||
},
|
||||
...entries.map(([path, target]) => {
|
||||
const data = Buffer.from(target!);
|
||||
const id = digest(data);
|
||||
blobs.set(id, data);
|
||||
return {
|
||||
path: path!,
|
||||
type: "symlink" as const,
|
||||
digest: id,
|
||||
size: data.length,
|
||||
mode: 0o777 as const,
|
||||
};
|
||||
}),
|
||||
],
|
||||
};
|
||||
const manifestData = Buffer.from(JSON.stringify(manifest));
|
||||
const manifestDigest = digest(manifestData);
|
||||
const reads: Sha256Digest[] = [];
|
||||
await expect(
|
||||
materializeWorkspace(
|
||||
{
|
||||
get: async (requested) => {
|
||||
reads.push(requested);
|
||||
if (requested === manifestDigest) return manifestData;
|
||||
if (requested === fileDigest)
|
||||
throw new Error("file blob read before validation");
|
||||
return blobs.get(requested)!;
|
||||
},
|
||||
},
|
||||
manifestDigest,
|
||||
destination,
|
||||
),
|
||||
).rejects.toThrow("Unsafe symlink target for chain");
|
||||
expect(reads).not.toContain(fileDigest);
|
||||
await expect(lstat(destination)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("materializes safe chained and parent-relative links", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "kuber-materialize-"));
|
||||
roots.push(root);
|
||||
const cas = new FilesystemCas(join(root, "cas"));
|
||||
const files = await Promise.all(
|
||||
[
|
||||
["nested/parent", "../file"],
|
||||
["chain", "nested/parent"],
|
||||
["repeat", "nested/../nested/parent"],
|
||||
].map(async ([path, target]) => {
|
||||
const data = Buffer.from(target!);
|
||||
return {
|
||||
path: path!,
|
||||
type: "symlink" as const,
|
||||
digest: await cas.put(data),
|
||||
size: data.length,
|
||||
mode: 0o777 as const,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const content = Buffer.from("safe");
|
||||
const manifest: WorkspaceManifest = {
|
||||
version: BUILD_PROTOCOL_VERSION,
|
||||
files: [
|
||||
...files,
|
||||
{
|
||||
path: "file",
|
||||
type: "file",
|
||||
digest: await cas.put(content),
|
||||
size: content.length,
|
||||
mode: 0o644,
|
||||
},
|
||||
],
|
||||
};
|
||||
const destination = join(root, "workspace");
|
||||
await materializeWorkspace(
|
||||
cas,
|
||||
await cas.put(Buffer.from(JSON.stringify(manifest))),
|
||||
destination,
|
||||
);
|
||||
expect(await readFile(join(destination, "chain"), "utf8")).toBe("safe");
|
||||
expect(await readFile(join(destination, "repeat"), "utf8")).toBe("safe");
|
||||
});
|
||||
|
||||
test("rejects direct escapes and symlink cycles before destination creation", async () => {
|
||||
for (const links of [
|
||||
[["nested/bad", "../../outside"]],
|
||||
[
|
||||
["a", "b"],
|
||||
["b", "a"],
|
||||
],
|
||||
[["self", "self"]],
|
||||
]) {
|
||||
const root = await mkdtemp(join(tmpdir(), "kuber-materialize-"));
|
||||
roots.push(root);
|
||||
const cas = new FilesystemCas(join(root, "cas"));
|
||||
const files = await Promise.all(
|
||||
links.map(async ([path, target]) => {
|
||||
const data = Buffer.from(target!);
|
||||
return {
|
||||
path: path!,
|
||||
type: "symlink" as const,
|
||||
digest: await cas.put(data),
|
||||
size: data.length,
|
||||
mode: 0o777 as const,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const manifest: WorkspaceManifest = {
|
||||
version: BUILD_PROTOCOL_VERSION,
|
||||
files,
|
||||
};
|
||||
const destination = join(root, "workspace");
|
||||
await expect(
|
||||
materializeWorkspace(
|
||||
cas,
|
||||
await cas.put(Buffer.from(JSON.stringify(manifest))),
|
||||
destination,
|
||||
),
|
||||
).rejects.toThrow(/Unsafe symlink/);
|
||||
await expect(lstat(destination)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects traversal, path collisions, unsafe links, and size mismatches atomically", async () => {
|
||||
expect(() =>
|
||||
parseWorkspaceManifest(
|
||||
|
||||
Reference in New Issue
Block a user