mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(ghidra): preserve configured seed path traversal
This commit is contained in:
1 parent
7ba1d0161e
commit
01d6a95fd7
2 files changed
+81
-3
No files matched your search
@@ -2,6 +2,8 @@ import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
open,
|
||||
readFile,
|
||||
realpath,
|
||||
rm,
|
||||
@@ -152,6 +154,70 @@ describe("Ghidra seed commitment", () => {
|
||||
expect(resolved.value.path).toBe(await realpath(path));
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"rejects an invalid file/.. traversal instead of reading a sibling seed file",
|
||||
async () => {
|
||||
const directory = await workspace();
|
||||
await writeFile(join(directory, "file"), "not a directory");
|
||||
await writeFile(join(directory, "seeds.tsv"), SEEDS);
|
||||
const configured = `${directory}/file/../seeds.tsv`;
|
||||
await expect(open(configured)).rejects.toMatchObject({ code: "ENOTDIR" });
|
||||
const resolved = await resolveGhidraAnalysisSeeds(configured, resources);
|
||||
expect(resolved.ok).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"preserves symlink/.. lookup against a sibling decoy",
|
||||
async () => {
|
||||
const directory = await workspace();
|
||||
const selected = join(directory, "selected");
|
||||
await mkdir(join(selected, "child"), { recursive: true });
|
||||
await writeFile(join(selected, "seeds.tsv"), SEEDS);
|
||||
await writeFile(join(directory, "seeds.tsv"), "0x1234\tcode\n");
|
||||
const link = join(directory, "link");
|
||||
await symlink(join(selected, "child"), link);
|
||||
const configured = `${link}/../seeds.tsv`;
|
||||
const resolved = await resolveGhidraAnalysisSeeds(configured, resources);
|
||||
if (!resolved.ok || resolved.value === undefined)
|
||||
throw new Error("Expected a commitment");
|
||||
expect(resolved.value.configured_path).toBe(configured);
|
||||
expect(resolved.value.path).toBe(
|
||||
await realpath(join(selected, "seeds.tsv")),
|
||||
);
|
||||
const snapshot = await snapshotGhidraAnalysisSeeds(
|
||||
resolved.value,
|
||||
await workspace(),
|
||||
resources,
|
||||
);
|
||||
expect(await readFile(snapshot, "utf8")).toBe(CANONICAL);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"rejects a configured traversal that becomes invalid before snapshotting",
|
||||
async () => {
|
||||
const directory = await workspace();
|
||||
const child = join(directory, "child");
|
||||
await mkdir(child);
|
||||
await writeFile(join(directory, "seeds.tsv"), SEEDS);
|
||||
const configured = `${child}/../seeds.tsv`;
|
||||
const resolved = await resolveGhidraAnalysisSeeds(configured, resources);
|
||||
if (!resolved.ok || resolved.value === undefined)
|
||||
throw new Error("Expected a commitment");
|
||||
await rm(child, { recursive: true });
|
||||
await writeFile(child, "not a directory");
|
||||
await expect(open(configured)).rejects.toMatchObject({ code: "ENOTDIR" });
|
||||
await expect(
|
||||
snapshotGhidraAnalysisSeeds(
|
||||
resolved.value,
|
||||
await workspace(),
|
||||
resources,
|
||||
),
|
||||
).rejects.toMatchObject({ _tag: "ProviderAdapterError" });
|
||||
},
|
||||
);
|
||||
|
||||
it("reports an unreadable seed file as a provider error", async () => {
|
||||
const resolved = await resolveGhidraAnalysisSeeds(
|
||||
join(await workspace(), "absent.tsv"),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, realpath } from "node:fs/promises";
|
||||
import { mkdir, realpath, stat } from "node:fs/promises";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
writeGhidraProfileArtifact,
|
||||
ghidraFileFailure,
|
||||
} from "./GhidraFileArtifacts.js";
|
||||
import { NonRegularFileReadError } from "../filesystem/RegularFile.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
/** Seed file format admitted by this adapter. */
|
||||
@@ -164,6 +165,15 @@ const readSeeds = (
|
||||
},
|
||||
);
|
||||
|
||||
/** Preserve native lookup semantics while allowing configured symbolic links. */
|
||||
const resolveSeedPath = async (configured: string): Promise<string> => {
|
||||
// Darwin realpath may collapse file/.. even though native lookup rejects it.
|
||||
const selected = await stat(configured);
|
||||
if (!selected.isFile())
|
||||
throw new NonRegularFileReadError(configured, selected.isDirectory());
|
||||
return realpath(configured);
|
||||
};
|
||||
|
||||
/** Resolve the configured seed file into an immutable profile commitment. */
|
||||
export const resolveGhidraAnalysisSeeds = async (
|
||||
configured: string | undefined,
|
||||
@@ -176,7 +186,7 @@ export const resolveGhidraAnalysisSeeds = async (
|
||||
let entries: readonly SeedEntry[];
|
||||
let path: string;
|
||||
try {
|
||||
path = await realpath(configured);
|
||||
path = await resolveSeedPath(configured);
|
||||
entries = await readSeeds(path, resources, signal);
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
@@ -215,7 +225,9 @@ export const snapshotGhidraAnalysisSeeds = async (
|
||||
resources
|
||||
.run(async () => {
|
||||
ghidraSeedCommitmentSchema.parse(commitment);
|
||||
if ((await realpath(commitment.configured_path)) !== commitment.path)
|
||||
if (
|
||||
(await resolveSeedPath(commitment.configured_path)) !== commitment.path
|
||||
)
|
||||
throw new Error(
|
||||
"Ghidra seed file resolved path differs from explicit configuration",
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user