fix(node-host): bind bun and deno approval scripts
This commit is contained in:
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Browser/SSRF: block private-network intermediate redirect hops in strict browser navigation flows and fail closed when remote tab-open paths cannot inspect redirect chains. Thanks @zpbrent.
|
||||
- MS Teams/authz: keep `groupPolicy: "allowlist"` enforcing sender allowlists even when a team/channel route allowlist is configured, so route matches no longer widen group access to every sender in that route. Thanks @zpbrent.
|
||||
- Security/system.run: bind approved `bun` and `deno run` script operands to on-disk file snapshots so post-approval script rewrites are denied before execution.
|
||||
|
||||
## 2026.3.8
|
||||
|
||||
|
||||
@@ -24,27 +24,68 @@ type HardeningCase = {
|
||||
checkRawCommandMatchesArgv?: boolean;
|
||||
};
|
||||
|
||||
function createScriptOperandFixture(tmp: string): {
|
||||
type ScriptOperandFixture = {
|
||||
command: string[];
|
||||
scriptPath: string;
|
||||
initialBody: string;
|
||||
} {
|
||||
if (process.platform === "win32") {
|
||||
const scriptPath = path.join(tmp, "run.js");
|
||||
expectedArgvIndex: number;
|
||||
};
|
||||
|
||||
type RuntimeFixture = {
|
||||
name: string;
|
||||
argv: string[];
|
||||
scriptName: string;
|
||||
initialBody: string;
|
||||
expectedArgvIndex: number;
|
||||
binName?: string;
|
||||
};
|
||||
|
||||
function createScriptOperandFixture(tmp: string, fixture?: RuntimeFixture): ScriptOperandFixture {
|
||||
if (fixture) {
|
||||
return {
|
||||
command: [process.execPath, "./run.js"],
|
||||
scriptPath,
|
||||
initialBody: 'console.log("SAFE");\n',
|
||||
command: fixture.argv,
|
||||
scriptPath: path.join(tmp, fixture.scriptName),
|
||||
initialBody: fixture.initialBody,
|
||||
expectedArgvIndex: fixture.expectedArgvIndex,
|
||||
};
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
return {
|
||||
command: [process.execPath, "./run.js"],
|
||||
scriptPath: path.join(tmp, "run.js"),
|
||||
initialBody: 'console.log("SAFE");\n',
|
||||
expectedArgvIndex: 1,
|
||||
};
|
||||
}
|
||||
const scriptPath = path.join(tmp, "run.sh");
|
||||
return {
|
||||
command: ["/bin/sh", "./run.sh"],
|
||||
scriptPath,
|
||||
scriptPath: path.join(tmp, "run.sh"),
|
||||
initialBody: "#!/bin/sh\necho SAFE\n",
|
||||
expectedArgvIndex: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function withFakeRuntimeBin<T>(params: { binName: string; run: () => T }): T {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `openclaw-${params.binName}-bin-`));
|
||||
const binDir = path.join(tmp, "bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
const runtimePath = path.join(binDir, params.binName);
|
||||
fs.writeFileSync(runtimePath, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
||||
fs.chmodSync(runtimePath, 0o755);
|
||||
const oldPath = process.env.PATH;
|
||||
process.env.PATH = `${binDir}${path.delimiter}${oldPath ?? ""}`;
|
||||
try {
|
||||
return params.run();
|
||||
} finally {
|
||||
if (oldPath === undefined) {
|
||||
delete process.env.PATH;
|
||||
} else {
|
||||
process.env.PATH = oldPath;
|
||||
}
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe("hardenApprovedExecutionPaths", () => {
|
||||
const cases: HardeningCase[] = [
|
||||
{
|
||||
@@ -150,6 +191,63 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
});
|
||||
}
|
||||
|
||||
const mutableOperandCases: RuntimeFixture[] = [
|
||||
{
|
||||
name: "bun direct file",
|
||||
binName: "bun",
|
||||
argv: ["bun", "./run.ts"],
|
||||
scriptName: "run.ts",
|
||||
initialBody: 'console.log("SAFE");\n',
|
||||
expectedArgvIndex: 1,
|
||||
},
|
||||
{
|
||||
name: "bun run file",
|
||||
binName: "bun",
|
||||
argv: ["bun", "run", "./run.ts"],
|
||||
scriptName: "run.ts",
|
||||
initialBody: 'console.log("SAFE");\n',
|
||||
expectedArgvIndex: 2,
|
||||
},
|
||||
{
|
||||
name: "deno run file with flags",
|
||||
binName: "deno",
|
||||
argv: ["deno", "run", "-A", "--allow-read", "--", "./run.ts"],
|
||||
scriptName: "run.ts",
|
||||
initialBody: 'console.log("SAFE");\n',
|
||||
expectedArgvIndex: 5,
|
||||
},
|
||||
];
|
||||
|
||||
for (const runtimeCase of mutableOperandCases) {
|
||||
it(`captures mutable ${runtimeCase.name} operands in approval plans`, () => {
|
||||
withFakeRuntimeBin({
|
||||
binName: runtimeCase.binName!,
|
||||
run: () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-approval-script-plan-"));
|
||||
const fixture = createScriptOperandFixture(tmp, runtimeCase);
|
||||
fs.writeFileSync(fixture.scriptPath, fixture.initialBody);
|
||||
try {
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: fixture.command,
|
||||
cwd: tmp,
|
||||
});
|
||||
expect(prepared.ok).toBe(true);
|
||||
if (!prepared.ok) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
expect(prepared.plan.mutableFileOperand).toEqual({
|
||||
argvIndex: fixture.expectedArgvIndex,
|
||||
path: fs.realpathSync(fixture.scriptPath),
|
||||
sha256: expect.any(String),
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("captures mutable shell script operands in approval plans", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-approval-script-plan-"));
|
||||
const fixture = createScriptOperandFixture(tmp);
|
||||
@@ -167,7 +265,7 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
expect(prepared.plan.mutableFileOperand).toEqual({
|
||||
argvIndex: 1,
|
||||
argvIndex: fixture.expectedArgvIndex,
|
||||
path: fs.realpathSync(fixture.scriptPath),
|
||||
sha256: expect.any(String),
|
||||
});
|
||||
@@ -175,4 +273,48 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not snapshot bun package script names", () => {
|
||||
withFakeRuntimeBin({
|
||||
binName: "bun",
|
||||
run: () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bun-package-script-"));
|
||||
try {
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: ["bun", "run", "dev"],
|
||||
cwd: tmp,
|
||||
});
|
||||
expect(prepared.ok).toBe(true);
|
||||
if (!prepared.ok) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
expect(prepared.plan.mutableFileOperand).toBeUndefined();
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not snapshot deno eval invocations", () => {
|
||||
withFakeRuntimeBin({
|
||||
binName: "deno",
|
||||
run: () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-deno-eval-"));
|
||||
try {
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: ["deno", "eval", "console.log('SAFE')"],
|
||||
cwd: tmp,
|
||||
});
|
||||
expect(prepared.ok).toBe(true);
|
||||
if (!prepared.ok) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
expect(prepared.plan.mutableFileOperand).toBeUndefined();
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,89 @@ const MUTABLE_ARGV1_INTERPRETER_PATTERNS = [
|
||||
/^ruby$/,
|
||||
] as const;
|
||||
|
||||
const BUN_SUBCOMMANDS = new Set([
|
||||
"add",
|
||||
"audit",
|
||||
"completions",
|
||||
"create",
|
||||
"exec",
|
||||
"help",
|
||||
"init",
|
||||
"install",
|
||||
"link",
|
||||
"outdated",
|
||||
"patch",
|
||||
"pm",
|
||||
"publish",
|
||||
"remove",
|
||||
"repl",
|
||||
"run",
|
||||
"test",
|
||||
"unlink",
|
||||
"update",
|
||||
"upgrade",
|
||||
"x",
|
||||
]);
|
||||
|
||||
const BUN_OPTIONS_WITH_VALUE = new Set([
|
||||
"--backend",
|
||||
"--bunfig",
|
||||
"--conditions",
|
||||
"--config",
|
||||
"--console-depth",
|
||||
"--cwd",
|
||||
"--define",
|
||||
"--elide-lines",
|
||||
"--env-file",
|
||||
"--extension-order",
|
||||
"--filter",
|
||||
"--hot",
|
||||
"--inspect",
|
||||
"--inspect-brk",
|
||||
"--inspect-wait",
|
||||
"--install",
|
||||
"--jsx-factory",
|
||||
"--jsx-fragment",
|
||||
"--jsx-import-source",
|
||||
"--loader",
|
||||
"--origin",
|
||||
"--port",
|
||||
"--preload",
|
||||
"--smol",
|
||||
"--tsconfig-override",
|
||||
"-c",
|
||||
"-e",
|
||||
"-p",
|
||||
"-r",
|
||||
]);
|
||||
|
||||
const DENO_RUN_OPTIONS_WITH_VALUE = new Set([
|
||||
"--cached-only",
|
||||
"--cert",
|
||||
"--config",
|
||||
"--env-file",
|
||||
"--ext",
|
||||
"--harmony-import-attributes",
|
||||
"--import-map",
|
||||
"--inspect",
|
||||
"--inspect-brk",
|
||||
"--inspect-wait",
|
||||
"--location",
|
||||
"--log-level",
|
||||
"--lock",
|
||||
"--node-modules-dir",
|
||||
"--no-check",
|
||||
"--preload",
|
||||
"--reload",
|
||||
"--seed",
|
||||
"--strace-ops",
|
||||
"--unstable-bare-node-builtins",
|
||||
"--v8-flags",
|
||||
"--watch",
|
||||
"--watch-exclude",
|
||||
"-L",
|
||||
]);
|
||||
|
||||
function normalizeString(value: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
@@ -94,6 +177,28 @@ function hashFileContentsSync(filePath: string): string {
|
||||
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
||||
}
|
||||
|
||||
function looksLikePathToken(token: string): boolean {
|
||||
return (
|
||||
token.startsWith(".") ||
|
||||
token.startsWith("/") ||
|
||||
token.startsWith("\\") ||
|
||||
token.includes("/") ||
|
||||
token.includes("\\") ||
|
||||
path.extname(token).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function resolvesToExistingFileSync(rawOperand: string, cwd: string | undefined): boolean {
|
||||
if (!rawOperand) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return fs.statSync(path.resolve(cwd ?? process.cwd(), rawOperand)).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapArgvForMutableOperand(argv: string[]): { argv: string[]; baseIndex: number } {
|
||||
let current = argv;
|
||||
let baseIndex = 0;
|
||||
@@ -146,7 +251,117 @@ function resolvePosixShellScriptOperandIndex(argv: string[]): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveMutableFileOperandIndex(argv: string[]): number | null {
|
||||
function resolveOptionFilteredFileOperandIndex(params: {
|
||||
argv: string[];
|
||||
startIndex: number;
|
||||
cwd: string | undefined;
|
||||
optionsWithValue?: ReadonlySet<string>;
|
||||
}): number | null {
|
||||
let afterDoubleDash = false;
|
||||
for (let i = params.startIndex; i < params.argv.length; i += 1) {
|
||||
const token = params.argv[i]?.trim() ?? "";
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
if (afterDoubleDash) {
|
||||
return resolvesToExistingFileSync(token, params.cwd) ? i : null;
|
||||
}
|
||||
if (token === "--") {
|
||||
afterDoubleDash = true;
|
||||
continue;
|
||||
}
|
||||
if (token === "-") {
|
||||
return null;
|
||||
}
|
||||
if (token.startsWith("-")) {
|
||||
if (!token.includes("=") && params.optionsWithValue?.has(token)) {
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return resolvesToExistingFileSync(token, params.cwd) ? i : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveOptionFilteredPositionalIndex(params: {
|
||||
argv: string[];
|
||||
startIndex: number;
|
||||
optionsWithValue?: ReadonlySet<string>;
|
||||
}): number | null {
|
||||
let afterDoubleDash = false;
|
||||
for (let i = params.startIndex; i < params.argv.length; i += 1) {
|
||||
const token = params.argv[i]?.trim() ?? "";
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
if (afterDoubleDash) {
|
||||
return i;
|
||||
}
|
||||
if (token === "--") {
|
||||
afterDoubleDash = true;
|
||||
continue;
|
||||
}
|
||||
if (token === "-") {
|
||||
return null;
|
||||
}
|
||||
if (token.startsWith("-")) {
|
||||
if (!token.includes("=") && params.optionsWithValue?.has(token)) {
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveBunScriptOperandIndex(params: {
|
||||
argv: string[];
|
||||
cwd: string | undefined;
|
||||
}): number | null {
|
||||
const directIndex = resolveOptionFilteredPositionalIndex({
|
||||
argv: params.argv,
|
||||
startIndex: 1,
|
||||
optionsWithValue: BUN_OPTIONS_WITH_VALUE,
|
||||
});
|
||||
if (directIndex === null) {
|
||||
return null;
|
||||
}
|
||||
const directToken = params.argv[directIndex]?.trim() ?? "";
|
||||
if (directToken === "run") {
|
||||
return resolveOptionFilteredFileOperandIndex({
|
||||
argv: params.argv,
|
||||
startIndex: directIndex + 1,
|
||||
cwd: params.cwd,
|
||||
optionsWithValue: BUN_OPTIONS_WITH_VALUE,
|
||||
});
|
||||
}
|
||||
if (BUN_SUBCOMMANDS.has(directToken)) {
|
||||
return null;
|
||||
}
|
||||
if (!looksLikePathToken(directToken)) {
|
||||
return null;
|
||||
}
|
||||
return directIndex;
|
||||
}
|
||||
|
||||
function resolveDenoRunScriptOperandIndex(params: {
|
||||
argv: string[];
|
||||
cwd: string | undefined;
|
||||
}): number | null {
|
||||
if ((params.argv[1]?.trim() ?? "") !== "run") {
|
||||
return null;
|
||||
}
|
||||
return resolveOptionFilteredFileOperandIndex({
|
||||
argv: params.argv,
|
||||
startIndex: 2,
|
||||
cwd: params.cwd,
|
||||
optionsWithValue: DENO_RUN_OPTIONS_WITH_VALUE,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveMutableFileOperandIndex(argv: string[], cwd: string | undefined): number | null {
|
||||
const unwrapped = unwrapArgvForMutableOperand(argv);
|
||||
const executable = normalizeExecutableToken(unwrapped.argv[0] ?? "");
|
||||
if (!executable) {
|
||||
@@ -157,6 +372,20 @@ function resolveMutableFileOperandIndex(argv: string[]): number | null {
|
||||
return shellIndex === null ? null : unwrapped.baseIndex + shellIndex;
|
||||
}
|
||||
if (!MUTABLE_ARGV1_INTERPRETER_PATTERNS.some((pattern) => pattern.test(executable))) {
|
||||
if (executable === "bun") {
|
||||
const bunIndex = resolveBunScriptOperandIndex({
|
||||
argv: unwrapped.argv,
|
||||
cwd,
|
||||
});
|
||||
return bunIndex === null ? null : unwrapped.baseIndex + bunIndex;
|
||||
}
|
||||
if (executable === "deno") {
|
||||
const denoIndex = resolveDenoRunScriptOperandIndex({
|
||||
argv: unwrapped.argv,
|
||||
cwd,
|
||||
});
|
||||
return denoIndex === null ? null : unwrapped.baseIndex + denoIndex;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const operand = unwrapped.argv[1]?.trim() ?? "";
|
||||
@@ -170,7 +399,7 @@ function resolveMutableFileOperandSnapshotSync(params: {
|
||||
argv: string[];
|
||||
cwd: string | undefined;
|
||||
}): { ok: true; snapshot: SystemRunApprovalFileOperand | null } | { ok: false; message: string } {
|
||||
const argvIndex = resolveMutableFileOperandIndex(params.argv);
|
||||
const argvIndex = resolveMutableFileOperandIndex(params.argv, params.cwd);
|
||||
if (argvIndex === null) {
|
||||
return { ok: true, snapshot: null };
|
||||
}
|
||||
|
||||
@@ -109,6 +109,29 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
};
|
||||
}
|
||||
|
||||
function createRuntimeScriptOperandFixture(params: { tmp: string; runtime: "bun" | "deno" }): {
|
||||
command: string[];
|
||||
scriptPath: string;
|
||||
initialBody: string;
|
||||
changedBody: string;
|
||||
} {
|
||||
const scriptPath = path.join(params.tmp, "run.ts");
|
||||
if (params.runtime === "bun") {
|
||||
return {
|
||||
command: ["bun", "run", "./run.ts"],
|
||||
scriptPath,
|
||||
initialBody: 'console.log("SAFE");\n',
|
||||
changedBody: 'console.log("PWNED");\n',
|
||||
};
|
||||
}
|
||||
return {
|
||||
command: ["deno", "run", "-A", "--allow-read", "--", "./run.ts"],
|
||||
scriptPath,
|
||||
initialBody: 'console.log("SAFE");\n',
|
||||
changedBody: 'console.log("PWNED");\n',
|
||||
};
|
||||
}
|
||||
|
||||
function buildNestedEnvShellCommand(params: { depth: number; payload: string }): string[] {
|
||||
return [...Array(params.depth).fill("/usr/bin/env"), "/bin/sh", "-c", params.payload];
|
||||
}
|
||||
@@ -199,6 +222,30 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function withFakeRuntimeOnPath<T>(params: {
|
||||
runtime: "bun" | "deno";
|
||||
run: () => Promise<T>;
|
||||
}): Promise<T> {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `openclaw-${params.runtime}-path-`));
|
||||
const binDir = path.join(tmp, "bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
const runtimePath = path.join(binDir, params.runtime);
|
||||
fs.writeFileSync(runtimePath, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
||||
fs.chmodSync(runtimePath, 0o755);
|
||||
const oldPath = process.env.PATH;
|
||||
process.env.PATH = `${binDir}${path.delimiter}${oldPath ?? ""}`;
|
||||
try {
|
||||
return await params.run();
|
||||
} finally {
|
||||
if (oldPath === undefined) {
|
||||
delete process.env.PATH;
|
||||
} else {
|
||||
process.env.PATH = oldPath;
|
||||
}
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function expectCommandPinnedToCanonicalPath(params: {
|
||||
runCommand: MockedRunCommand;
|
||||
expected: string;
|
||||
@@ -788,6 +835,90 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
}
|
||||
});
|
||||
|
||||
for (const runtime of ["bun", "deno"] as const) {
|
||||
it(`denies approval-based execution when a ${runtime} script operand changes after approval`, async () => {
|
||||
await withFakeRuntimeOnPath({
|
||||
runtime,
|
||||
run: async () => {
|
||||
const tmp = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), `openclaw-approval-${runtime}-script-drift-`),
|
||||
);
|
||||
const fixture = createRuntimeScriptOperandFixture({ tmp, runtime });
|
||||
fs.writeFileSync(fixture.scriptPath, fixture.initialBody);
|
||||
try {
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: fixture.command,
|
||||
cwd: tmp,
|
||||
});
|
||||
expect(prepared.ok).toBe(true);
|
||||
if (!prepared.ok) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
fs.writeFileSync(fixture.scriptPath, fixture.changedBody);
|
||||
const { runCommand, sendInvokeResult } = await runSystemInvoke({
|
||||
preferMacAppExecHost: false,
|
||||
command: prepared.plan.argv,
|
||||
rawCommand: prepared.plan.rawCommand,
|
||||
systemRunPlan: prepared.plan,
|
||||
cwd: prepared.plan.cwd ?? tmp,
|
||||
approved: true,
|
||||
security: "full",
|
||||
ask: "off",
|
||||
});
|
||||
|
||||
expect(runCommand).not.toHaveBeenCalled();
|
||||
expectInvokeErrorMessage(sendInvokeResult, {
|
||||
message: "SYSTEM_RUN_DENIED: approval script operand changed before execution",
|
||||
exact: true,
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`keeps approved ${runtime} script execution working when the script is unchanged`, async () => {
|
||||
await withFakeRuntimeOnPath({
|
||||
runtime,
|
||||
run: async () => {
|
||||
const tmp = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), `openclaw-approval-${runtime}-script-stable-`),
|
||||
);
|
||||
const fixture = createRuntimeScriptOperandFixture({ tmp, runtime });
|
||||
fs.writeFileSync(fixture.scriptPath, fixture.initialBody);
|
||||
try {
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: fixture.command,
|
||||
cwd: tmp,
|
||||
});
|
||||
expect(prepared.ok).toBe(true);
|
||||
if (!prepared.ok) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
const { runCommand, sendInvokeResult } = await runSystemInvoke({
|
||||
preferMacAppExecHost: false,
|
||||
command: prepared.plan.argv,
|
||||
rawCommand: prepared.plan.rawCommand,
|
||||
systemRunPlan: prepared.plan,
|
||||
cwd: prepared.plan.cwd ?? tmp,
|
||||
approved: true,
|
||||
security: "full",
|
||||
ask: "off",
|
||||
});
|
||||
|
||||
expect(runCommand).toHaveBeenCalledTimes(1);
|
||||
expectInvokeOk(sendInvokeResult);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("denies ./sh wrapper spoof in allowlist on-miss mode before execution", async () => {
|
||||
const marker = path.join(os.tmpdir(), `openclaw-wrapper-spoof-${process.pid}-${Date.now()}`);
|
||||
const runCommand = vi.fn(async () => {
|
||||
|
||||
Reference in New Issue
Block a user