

















11import fs from "node:fs";
22import os from "node:os";
33import path from "node:path";
4-import { describe, expect, it } from "vitest";
4+import { describe, expect, it, vi } from "vitest";
55import { formatExecCommand } from "../infra/system-run-command.js";
66import {
77buildSystemRunApprovalPlan,
@@ -122,6 +122,43 @@ function withFakeRuntimeBins<T>(params: {
122122}
123123}
124124125+function resolveNativeBinaryFixturePath(): string {
126+for (const candidate of ["/bin/ls", "/usr/bin/ls", "/bin/echo", "/usr/bin/printf"]) {
127+try {
128+if (fs.statSync(candidate).isFile()) {
129+return candidate;
130+}
131+} catch {
132+continue;
133+}
134+}
135+throw new Error("expected a native binary fixture path");
136+}
137+138+function expectShellPayloadApprovalDenied(params: {
139+tmpPrefix: string;
140+fileName: string;
141+body: string;
142+}) {
143+if (process.platform === "win32") {
144+return;
145+}
146+const tmp = fs.mkdtempSync(path.join(os.tmpdir(), params.tmpPrefix));
147+try {
148+const scriptPath = path.join(tmp, params.fileName);
149+fs.writeFileSync(scriptPath, params.body);
150+fs.chmodSync(scriptPath, 0o755);
151+const prepared = buildSystemRunApprovalPlan({
152+command: ["/bin/sh", "-lc", scriptPath],
153+rawCommand: scriptPath,
154+cwd: tmp,
155+});
156+expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
157+} finally {
158+fs.rmSync(tmp, { recursive: true, force: true });
159+}
160+}
161+125162function expectMutableFileOperandApprovalPlan(fixture: ScriptOperandFixture, cwd: string) {
126163const prepared = buildSystemRunApprovalPlan({
127164command: fixture.command,
@@ -769,6 +806,162 @@ describe("hardenApprovedExecutionPaths", () => {
769806);
770807});
771808809+it("allows shell payloads that invoke absolute-path native binaries", () => {
810+if (process.platform === "win32") {
811+return;
812+}
813+const binaryPath = resolveNativeBinaryFixturePath();
814+const prepared = buildSystemRunApprovalPlan({
815+command: ["/bin/sh", "-lc", binaryPath],
816+rawCommand: binaryPath,
817+cwd: process.cwd(),
818+});
819+expect(prepared.ok).toBe(true);
820+if (!prepared.ok) {
821+throw new Error("unreachable");
822+}
823+expect(prepared.plan.mutableFileOperand).toBeUndefined();
824+});
825+826+it("keeps fail-closed behavior for relative native-binary shell payloads", () => {
827+if (process.platform === "win32") {
828+return;
829+}
830+const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-shell-relative-binary-binding-"));
831+try {
832+const binaryPath = resolveNativeBinaryFixturePath();
833+const relativeBinaryPath = path.join(tmp, "tool");
834+fs.copyFileSync(binaryPath, relativeBinaryPath);
835+fs.chmodSync(relativeBinaryPath, 0o755);
836+const prepared = buildSystemRunApprovalPlan({
837+command: ["/bin/sh", "-lc", "./tool"],
838+rawCommand: "./tool",
839+cwd: tmp,
840+});
841+expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
842+} finally {
843+fs.rmSync(tmp, { recursive: true, force: true });
844+}
845+});
846+847+it("keeps fail-closed behavior for writable absolute native-binary shell payloads", () => {
848+if (process.platform === "win32") {
849+return;
850+}
851+const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-shell-absolute-binary-binding-"));
852+try {
853+const binaryPath = resolveNativeBinaryFixturePath();
854+const copiedBinaryPath = path.join(tmp, "tool");
855+fs.copyFileSync(binaryPath, copiedBinaryPath);
856+fs.chmodSync(copiedBinaryPath, 0o755);
857+const prepared = buildSystemRunApprovalPlan({
858+command: ["/bin/sh", "-lc", copiedBinaryPath],
859+rawCommand: copiedBinaryPath,
860+cwd: tmp,
861+});
862+expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
863+} finally {
864+fs.rmSync(tmp, { recursive: true, force: true });
865+}
866+});
867+868+it("keeps fail-closed behavior for symlinked binaries with writable targets", () => {
869+if (process.platform === "win32") {
870+return;
871+}
872+const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-shell-symlink-binary-binding-"));
873+const stableDir = path.join(tmp, "stable");
874+const mutableDir = path.join(tmp, "mutable");
875+try {
876+const binaryPath = resolveNativeBinaryFixturePath();
877+fs.mkdirSync(stableDir);
878+fs.mkdirSync(mutableDir);
879+const targetBinaryPath = path.join(mutableDir, "tool");
880+const symlinkPath = path.join(stableDir, "tool");
881+fs.copyFileSync(binaryPath, targetBinaryPath);
882+fs.chmodSync(targetBinaryPath, 0o755);
883+fs.symlinkSync(targetBinaryPath, symlinkPath);
884+fs.chmodSync(stableDir, 0o555);
885+const prepared = buildSystemRunApprovalPlan({
886+command: ["/bin/sh", "-lc", symlinkPath],
887+rawCommand: symlinkPath,
888+cwd: tmp,
889+});
890+expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
891+} finally {
892+fs.chmodSync(stableDir, 0o755);
893+fs.rmSync(tmp, { recursive: true, force: true });
894+}
895+});
896+897+it("keeps fail-closed behavior for shell payloads that invoke mutable script files", () => {
898+expectShellPayloadApprovalDenied({
899+tmpPrefix: "openclaw-shell-script-binding-",
900+fileName: "run.sh",
901+body: "#!/bin/sh\necho SAFE\n",
902+});
903+});
904+905+it("keeps fail-closed behavior for empty shell payload files", () => {
906+expectShellPayloadApprovalDenied({
907+tmpPrefix: "openclaw-shell-empty-binding-",
908+fileName: "empty",
909+body: "",
910+});
911+});
912+913+it("does not treat weak MZ text headers as native binaries", () => {
914+expectShellPayloadApprovalDenied({
915+tmpPrefix: "openclaw-shell-mz-text-binding-",
916+fileName: "mz-script",
917+body: "MZ not really a PE file\n",
918+});
919+});
920+921+it("keeps fail-closed behavior for unknown NUL-bearing headers", () => {
922+expectShellPayloadApprovalDenied({
923+tmpPrefix: "openclaw-shell-nul-header-binding-",
924+fileName: "nul-script",
925+body: "SAFE\u0000maybe-binary\n",
926+});
927+});
928+929+it("keeps fail-closed behavior when the shell payload probe stops seeing a file", () => {
930+if (process.platform === "win32") {
931+return;
932+}
933+const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-shell-race-binding-"));
934+try {
935+const scriptPath = path.join(tmp, "run.sh");
936+fs.writeFileSync(scriptPath, "#!/bin/sh\necho SAFE\n");
937+fs.chmodSync(scriptPath, 0o755);
938+const realStatSync = fs.statSync;
939+let targetStatCalls = 0;
940+const statSyncSpy = vi.spyOn(fs, "statSync").mockImplementation((pathLike, options) => {
941+const targetPath = typeof pathLike === "string" ? pathLike : pathLike.toString();
942+if (targetPath === scriptPath) {
943+targetStatCalls += 1;
944+if (targetStatCalls === 2) {
945+return realStatSync(tmp, options);
946+}
947+}
948+return realStatSync(pathLike, options);
949+});
950+try {
951+const prepared = buildSystemRunApprovalPlan({
952+command: ["/bin/sh", "-lc", scriptPath],
953+rawCommand: scriptPath,
954+cwd: tmp,
955+});
956+expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
957+} finally {
958+statSyncSpy.mockRestore();
959+}
960+} finally {
961+fs.rmSync(tmp, { recursive: true, force: true });
962+}
963+});
964+772965it.each(unsafeRuntimeInvocationCases)("$name", (testCase) => {
773966withFakeRuntimeBin({
774967binName: testCase.binName,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。