mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(firmware): admit caller-selected source directly
This commit is contained in:
1 parent
a905a71e31
commit
c1e0f531e6
2 files changed
+124
-9
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { safeParseUtf8Json } from "../domain/safeJson.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { chmod, lstat, opendir, readlink, realpath } from "node:fs/promises";
|
||||
import { chmod, lstat, opendir, readlink } from "node:fs/promises";
|
||||
import { isAbsolute, join, relative, sep } from "node:path";
|
||||
import { Readable, Transform } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
@@ -113,17 +113,10 @@ export class FirmwareFileAccess {
|
||||
|
||||
async #snapshot(request: FirmwareRequest, root: string, signal: AbortSignal) {
|
||||
const sourcePath = absoluteCallerPath(request.input.path);
|
||||
const source = await realpath(sourcePath).catch((cause: unknown) => {
|
||||
throw firmwareSourceAdmissionFailure(
|
||||
request.operation,
|
||||
sourcePath,
|
||||
cause,
|
||||
);
|
||||
});
|
||||
let sourceAdmitted = false;
|
||||
const fullPath = join(root, "firmware.bin");
|
||||
const copied = await withArtifactRegularFile(
|
||||
source,
|
||||
sourcePath,
|
||||
this.#resources,
|
||||
signal,
|
||||
async ({ handle, initial: stat }) => {
|
||||
@@ -150,6 +143,7 @@ export class FirmwareFileAccess {
|
||||
);
|
||||
return { size, sha256: hash.digest("hex") };
|
||||
},
|
||||
"follow",
|
||||
).catch((cause: unknown) => {
|
||||
// Destination writes and reads after admission retain their own failure.
|
||||
throw sourceAdmitted
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { firmwareRequestSchema } from "../domain/firmware/firmwareAnalysis.js";
|
||||
import { FirmwareFileAccess } from "./FirmwareFiles.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
roots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
const workspace = async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-firmware-source-"));
|
||||
roots.push(root);
|
||||
const staging = join(root, "stage");
|
||||
await mkdir(staging);
|
||||
return { root, staging };
|
||||
};
|
||||
|
||||
it("honors cancellation before source admission without creating a snapshot", async () => {
|
||||
const { root, staging } = await workspace();
|
||||
const path = join(root, "firmware");
|
||||
await writeFile(path, "firmware");
|
||||
const request = firmwareRequestSchema.parse({
|
||||
operation: "inspect_firmware_regions",
|
||||
input: { path },
|
||||
});
|
||||
const cancellation = new AbortController();
|
||||
cancellation.abort(new Error("cancel source admission"));
|
||||
const files = new FirmwareFileAccess();
|
||||
try {
|
||||
await expect(
|
||||
files.snapshot(request, staging, cancellation.signal),
|
||||
).rejects.toMatchObject({ reason: "cancelled" });
|
||||
await expect(readFile(join(staging, "firmware.bin"))).rejects.toMatchObject(
|
||||
{ code: "ENOENT" },
|
||||
);
|
||||
} finally {
|
||||
await files.close();
|
||||
}
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"rejects file/.. traversal before creating a firmware snapshot",
|
||||
async () => {
|
||||
const { root, staging } = await workspace();
|
||||
await writeFile(join(root, "blocker"), "regular file");
|
||||
await writeFile(join(root, "firmware"), "decoy firmware");
|
||||
const path = `${root}/blocker/../firmware`;
|
||||
await expect(readFile(path)).rejects.toMatchObject({ code: "ENOTDIR" });
|
||||
const request = firmwareRequestSchema.parse({
|
||||
operation: "inspect_firmware_regions",
|
||||
input: { path },
|
||||
});
|
||||
const files = new FirmwareFileAccess();
|
||||
try {
|
||||
await expect(
|
||||
files.snapshot(request, staging, new AbortController().signal),
|
||||
).rejects.toMatchObject({ _tag: "AnalysisInputError" });
|
||||
await expect(
|
||||
readFile(join(staging, "firmware.bin")),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
} finally {
|
||||
await files.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// POSIX parent traversal and symlink creation require host filesystem support;
|
||||
// Windows does not guarantee either traversal semantics or symlink privileges.
|
||||
it.skipIf(process.platform === "win32").each(["parent", "final"] as const)(
|
||||
"snapshots the filesystem-selected %s symlink target and preserves its selected path",
|
||||
async (kind) => {
|
||||
const { root, staging } = await workspace();
|
||||
await mkdir(join(root, "physical", "child"), { recursive: true });
|
||||
const bytes = Buffer.from("selected firmware bytes");
|
||||
await writeFile(join(root, "physical", "firmware"), bytes);
|
||||
await writeFile(join(root, "firmware"), "lexical decoy");
|
||||
await symlink(join(root, "physical", "child"), join(root, "link"), "dir");
|
||||
await symlink(
|
||||
join(root, "physical", "firmware"),
|
||||
join(root, "alias"),
|
||||
"file",
|
||||
);
|
||||
const path =
|
||||
kind === "parent" ? `${root}/link/../firmware` : join(root, "alias");
|
||||
const request = firmwareRequestSchema.parse({
|
||||
operation: "inspect_firmware_regions",
|
||||
input: { path },
|
||||
});
|
||||
const files = new FirmwareFileAccess();
|
||||
try {
|
||||
const snapshot = await files.snapshot(
|
||||
request,
|
||||
staging,
|
||||
new AbortController().signal,
|
||||
);
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
expect(snapshot.subject).toEqual({ path, sha256, format: "file" });
|
||||
expect(snapshot.selection).toEqual({
|
||||
offset: 0,
|
||||
length: bytes.length,
|
||||
sha256,
|
||||
});
|
||||
expect((await readFile(snapshot.inputPath)).equals(bytes)).toBe(true);
|
||||
} finally {
|
||||
await files.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user