fix(boundaries): reject unsafe local inputs

This commit is contained in:
morluto 2026-07-12 08:59:18 +08:00
parent aee9cb8e4e
commit 15e8cf3234
4 changed files with 26 additions and 3 deletions

View File

@ -308,7 +308,7 @@ const objectValue = (value: unknown): Record<string, unknown> => {
return parsed.success ? parsed.data : {};
};
const parseObject = (text: string): Record<string, unknown> =>
objectValue(JSON.parse(text));
objectSchema.parse(JSON.parse(text));
const isMissing = (cause: unknown): boolean =>
cause instanceof Error && "code" in cause && cause.code === "ENOENT";
const restoreConfig = async (

View File

@ -1,5 +1,5 @@
import { constants } from "node:fs";
import { access, open, realpath } from "node:fs/promises";
import { access, open, realpath, stat } from "node:fs/promises";
import { isAbsolute, resolve } from "node:path";
import { BinaryTargetError } from "./errors.js";
@ -36,6 +36,8 @@ export const parseBinaryTarget = async (
try {
await access(candidate, constants.R_OK);
const path = await realpath(candidate);
if (!(await stat(path)).isFile())
return err(new BinaryTargetError(path, "target is not a regular file"));
if (
targetKind === "database" ||
(targetKind === undefined && path.toLowerCase().endsWith(".hop"))

View File

@ -67,7 +67,7 @@ describe("executable header parsing", () => {
describe("binary target I/O", () => {
it("resolves relative Hopper databases and rejects unknown or unreadable paths", async () => {
directory = await mkdtemp(join(tmpdir(), "bb-target-"));
directory = await mkdtemp(join(tmpdir(), "rea-target-"));
await writeFile(join(directory, "sample.hop"), "database");
await writeFile(join(directory, "text"), "hello");
expect((await parseBinaryTarget("sample.hop", directory)).ok).toBe(true);
@ -75,6 +75,11 @@ describe("binary target I/O", () => {
expect((await parseBinaryTarget("missing", directory)).ok).toBe(false);
});
it("rejects non-regular targets before reading them", async () => {
directory = await mkdtemp(join(tmpdir(), "rea-target-"));
expect((await parseBinaryTarget(directory)).ok).toBe(false);
});
it("honors an explicit database kind without relying on the file suffix", async () => {
directory = await mkdtemp(join(tmpdir(), "rea-target-"));
await writeFile(join(directory, "saved-analysis"), "database");

View File

@ -59,4 +59,20 @@ describe("JSON client configuration transaction", () => {
});
expect(await readFile(configPath, "utf8")).toBe("not-json");
});
it.each(["null", "[]", '"value"'])(
"refuses a non-object JSON root without overwriting %s",
async (original) => {
directory = await mkdtemp(join(tmpdir(), "rea-setup-"));
const configPath = join(directory, "mcp.json");
await writeFile(configPath, original);
expect(await configureJsonClient({ name: "cursor", configPath })).toEqual(
{ status: "failed", reason: "readback" },
);
expect(await readFile(configPath, "utf8")).toBe(original);
await expect(
readFile(`${configPath}.rea.backup`, "utf8"),
).rejects.toThrow();
},
);
});