mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04:00
fix(ida): preserve path identity across symlink traversal
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
import { readFile, readdir, rm } from "node:fs/promises";
|
||||
import { sep } from "node:path";
|
||||
import {
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
realpath,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { basename, dirname, join, sep } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
@@ -356,6 +364,75 @@ describe("attached IDA input identity and validation", () => {
|
||||
await client.close();
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"rejects a document whose symlink traversal names a different input",
|
||||
async () => {
|
||||
const { root, target, producer, client } = await fixture();
|
||||
await mkdir(join(root, "foreign", "inner"), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "foreign", basename(target.path)),
|
||||
"foreign input",
|
||||
);
|
||||
await symlink(join(root, "foreign", "inner"), join(root, "link"));
|
||||
const document = `${root}/link/../${basename(target.path)}`;
|
||||
expect(await realpath(document)).not.toBe(await realpath(target.path));
|
||||
|
||||
const result = await client.execute("list_procedures", { document });
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisInputError" },
|
||||
});
|
||||
expect(producer.connects).toBe(0);
|
||||
await client.close();
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"refuses an opened worker whose input only matches after lexical parent collapse",
|
||||
async () => {
|
||||
const { root, producer, client } = await fixture("headless");
|
||||
await mkdir(join(root, "foreign", "inner"), { recursive: true });
|
||||
let privateInput = "";
|
||||
producer.beforeCall = async (name, args) => {
|
||||
if (name !== "idb_open") return;
|
||||
privateInput = z.string().parse(args.input_path);
|
||||
await writeFile(
|
||||
join(root, "foreign", basename(privateInput)),
|
||||
"foreign input",
|
||||
);
|
||||
await symlink(
|
||||
join(root, "foreign", "inner"),
|
||||
join(dirname(privateInput), "link"),
|
||||
);
|
||||
};
|
||||
producer.overrideCall = (name, args) => {
|
||||
if (name !== "idb_open") return undefined;
|
||||
producer.database = z.string().parse(args.preferred_session_id);
|
||||
producer.inputPath = `${dirname(privateInput)}/link/../${basename(privateInput)}`;
|
||||
return {
|
||||
success: true,
|
||||
session: {
|
||||
session_id: producer.database,
|
||||
input_path: producer.inputPath,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const result = await client.execute("list_procedures", {});
|
||||
|
||||
expect(await realpath(producer.inputPath)).not.toBe(
|
||||
await realpath(privateInput),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(producer.calls.some(({ name }) => name === "list_funcs")).toBe(
|
||||
false,
|
||||
);
|
||||
producer.inputPath = privateInput;
|
||||
await client.close();
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects an unrelated document before connecting or opening a provider", async () => {
|
||||
const { producer, client } = await fixture();
|
||||
const result = await client.execute("list_procedures", {
|
||||
|
||||
@@ -2,7 +2,8 @@ import { createHash, randomUUID } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { open, realpath, stat, type FileHandle } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, join, normalize } from "node:path";
|
||||
import { basename, isAbsolute, join, normalize } from "node:path";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { AnalysisProtocolError } from "../domain/analysisErrorCore.js";
|
||||
import { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
@@ -21,11 +22,12 @@ export const idaFileDigest = async (path: string): Promise<string> => {
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
/** Compare host-local paths without confusing Windows paths with POSIX coordinates. */
|
||||
/** Compare host-local spellings without collapsing POSIX symlink/.. traversal. */
|
||||
export const sameIdaHostPath = (left: string, right: string): boolean =>
|
||||
process.platform === "win32"
|
||||
? normalize(left).toLowerCase() === normalize(right).toLowerCase()
|
||||
: normalize(left) === normalize(right);
|
||||
: isAbsolute(left) === isAbsolute(right) &&
|
||||
absoluteCallerPath(left) === absoluteCallerPath(right);
|
||||
|
||||
/** Admit the selected lookup before canonicalizing it for readiness and allocation.
|
||||
* Native realpath can collapse file/.. without rejecting the non-directory.
|
||||
|
||||
Reference in New Issue
Block a user