mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-28 18:33:37 +00:00
infra: unwrap script wrapper approval targets (#55685)
* infra: unwrap script wrapper approvals * infra: handle script short option values * infra: gate script wrapper unwrapping by platform * infra: narrow script wrapper option parsing
This commit is contained in:
@@ -42,6 +42,8 @@ const TIME_FLAG_OPTIONS = new Set([
|
||||
"--version",
|
||||
]);
|
||||
const TIME_OPTIONS_WITH_VALUE = new Set(["-f", "--format", "-o", "--output"]);
|
||||
const BSD_SCRIPT_FLAG_OPTIONS = new Set(["-a", "-d", "-k", "-p", "-q", "-r"]);
|
||||
const BSD_SCRIPT_OPTIONS_WITH_VALUE = new Set(["-F", "-t"]);
|
||||
const TIMEOUT_FLAG_OPTIONS = new Set(["--foreground", "--preserve-status", "-v", "--verbose"]);
|
||||
const TIMEOUT_OPTIONS_WITH_VALUE = new Set(["-k", "--kill-after", "-s", "--signal"]);
|
||||
|
||||
@@ -259,6 +261,47 @@ function unwrapTimeInvocation(argv: string[]): string[] | null {
|
||||
});
|
||||
}
|
||||
|
||||
function supportsScriptPositionalCommand(platform: NodeJS.Platform = process.platform): boolean {
|
||||
return platform === "darwin" || platform === "freebsd";
|
||||
}
|
||||
|
||||
function unwrapScriptInvocation(argv: string[]): string[] | null {
|
||||
if (!supportsScriptPositionalCommand()) {
|
||||
return null;
|
||||
}
|
||||
return scanWrapperInvocation(argv, {
|
||||
separators: new Set(["--"]),
|
||||
onToken: (token, lower) => {
|
||||
if (!lower.startsWith("-") || lower === "-") {
|
||||
return "stop";
|
||||
}
|
||||
const [flag] = token.split("=", 2);
|
||||
if (BSD_SCRIPT_OPTIONS_WITH_VALUE.has(flag)) {
|
||||
return token.includes("=") ? "continue" : "consume-next";
|
||||
}
|
||||
if (BSD_SCRIPT_FLAG_OPTIONS.has(flag)) {
|
||||
return "continue";
|
||||
}
|
||||
return "invalid";
|
||||
},
|
||||
adjustCommandIndex: (commandIndex, currentArgv) => {
|
||||
let sawTranscript = false;
|
||||
for (let idx = commandIndex; idx < currentArgv.length; idx += 1) {
|
||||
const token = currentArgv[idx]?.trim() ?? "";
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
if (!sawTranscript) {
|
||||
sawTranscript = true;
|
||||
continue;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function unwrapTimeoutInvocation(argv: string[]): string[] | null {
|
||||
return unwrapDashOptionInvocation(argv, {
|
||||
onFlag: (flag, lower) => {
|
||||
@@ -294,6 +337,7 @@ const DISPATCH_WRAPPER_SPECS: readonly DispatchWrapperSpec[] = [
|
||||
{ name: "ionice" },
|
||||
{ name: "nice", unwrap: unwrapNiceInvocation, transparentUsage: true },
|
||||
{ name: "nohup", unwrap: unwrapNohupInvocation, transparentUsage: true },
|
||||
{ name: "script", unwrap: unwrapScriptInvocation, transparentUsage: true },
|
||||
{ name: "setsid" },
|
||||
{ name: "stdbuf", unwrap: unwrapStdbufInvocation, transparentUsage: true },
|
||||
{ name: "sudo" },
|
||||
|
||||
@@ -619,6 +619,23 @@ $0 \\"$1\\"" touch {marker}`,
|
||||
});
|
||||
});
|
||||
|
||||
it("prevents allow-always bypass for script wrapper chains", () => {
|
||||
if (process.platform !== "darwin" && process.platform !== "freebsd") {
|
||||
return;
|
||||
}
|
||||
const dir = makeTempDir();
|
||||
const echo = makeExecutable(dir, "echo");
|
||||
makeExecutable(dir, "id");
|
||||
const env = makePathEnv(dir);
|
||||
expectAllowAlwaysBypassBlocked({
|
||||
dir,
|
||||
firstCommand: "/usr/bin/script -q /dev/null /bin/sh -lc 'echo warmup-ok'",
|
||||
secondCommand: "/usr/bin/script -q /dev/null /bin/sh -lc 'id > marker'",
|
||||
env,
|
||||
persistedPattern: echo,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not persist comment-tailed payload paths that never execute", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
|
||||
@@ -13,6 +13,10 @@ import {
|
||||
unwrapKnownShellMultiplexerInvocation,
|
||||
} from "./exec-wrapper-resolution.js";
|
||||
|
||||
function supportsScriptPositionalCommandForTests(): boolean {
|
||||
return process.platform === "darwin" || process.platform === "freebsd";
|
||||
}
|
||||
|
||||
describe("basenameLower", () => {
|
||||
test.each([
|
||||
{ token: " Bun.CMD ", expected: "bun.cmd" },
|
||||
@@ -40,6 +44,7 @@ describe("normalizeExecutableToken", () => {
|
||||
describe("wrapper classification", () => {
|
||||
test.each([
|
||||
{ token: "sudo", dispatch: true, shell: false },
|
||||
{ token: "script", dispatch: true, shell: false },
|
||||
{ token: "time", dispatch: true, shell: false },
|
||||
{ token: "timeout.exe", dispatch: true, shell: false },
|
||||
{ token: "bash", dispatch: false, shell: true },
|
||||
@@ -119,6 +124,16 @@ describe("unwrapKnownDispatchWrapperInvocation", () => {
|
||||
argv: ["nohup", "--", "bash", "-lc", "echo hi"],
|
||||
expected: { kind: "unwrapped", wrapper: "nohup", argv: ["bash", "-lc", "echo hi"] },
|
||||
},
|
||||
{
|
||||
argv: ["script", "-q", "/dev/null", "bash", "-lc", "echo hi"],
|
||||
expected: supportsScriptPositionalCommandForTests()
|
||||
? { kind: "unwrapped", wrapper: "script", argv: ["bash", "-lc", "echo hi"] }
|
||||
: { kind: "blocked", wrapper: "script" },
|
||||
},
|
||||
{
|
||||
argv: ["script", "-E", "always", "/dev/null", "bash", "-lc", "echo hi"],
|
||||
expected: { kind: "blocked", wrapper: "script" },
|
||||
},
|
||||
{
|
||||
argv: ["stdbuf", "-o", "L", "bash", "-lc", "echo hi"],
|
||||
expected: { kind: "unwrapped", wrapper: "stdbuf", argv: ["bash", "-lc", "echo hi"] },
|
||||
@@ -131,6 +146,10 @@ describe("unwrapKnownDispatchWrapperInvocation", () => {
|
||||
argv: ["timeout", "--signal=TERM", "5s", "bash", "-lc", "echo hi"],
|
||||
expected: { kind: "unwrapped", wrapper: "timeout", argv: ["bash", "-lc", "echo hi"] },
|
||||
},
|
||||
{
|
||||
argv: ["script", "-q", "/dev/null"],
|
||||
expected: { kind: "blocked", wrapper: "script" },
|
||||
},
|
||||
{
|
||||
argv: ["sudo", "bash", "-lc", "echo hi"],
|
||||
expected: { kind: "blocked", wrapper: "sudo" },
|
||||
|
||||
@@ -18,6 +18,22 @@ describe("resolveExecWrapperTrustPlan", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("unwraps script wrappers before evaluating nested shell payloads", () => {
|
||||
if (process.platform !== "darwin" && process.platform !== "freebsd") {
|
||||
return;
|
||||
}
|
||||
expect(
|
||||
resolveExecWrapperTrustPlan(["/usr/bin/script", "-q", "/dev/null", "sh", "-lc", "echo hi"]),
|
||||
).toEqual({
|
||||
argv: ["sh", "-lc", "echo hi"],
|
||||
policyArgv: ["sh", "-lc", "echo hi"],
|
||||
wrapperChain: ["script"],
|
||||
policyBlocked: false,
|
||||
shellWrapperExecutable: true,
|
||||
shellInlineCommand: "echo hi",
|
||||
});
|
||||
});
|
||||
|
||||
test("fails closed for unsupported shell multiplexer applets", () => {
|
||||
expect(resolveExecWrapperTrustPlan(["busybox", "sed", "-n", "1p"])).toEqual({
|
||||
argv: ["busybox", "sed", "-n", "1p"],
|
||||
|
||||
Reference in New Issue
Block a user