mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-24 08:21:39 +00:00
fix(security): unwrap time dispatch wrappers
This commit is contained in:
@@ -318,6 +318,32 @@ describe("resolveAllowAlwaysPatterns", () => {
|
||||
expect(patterns).not.toContain("/usr/bin/nice");
|
||||
});
|
||||
|
||||
it("unwraps time wrappers and persists the inner executable instead", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const dir = makeTempDir();
|
||||
const whoami = makeExecutable(dir, "whoami");
|
||||
const patterns = resolveAllowAlwaysPatterns({
|
||||
segments: [
|
||||
{
|
||||
raw: "/usr/bin/time -p /bin/zsh -lc whoami",
|
||||
argv: ["/usr/bin/time", "-p", "/bin/zsh", "-lc", "whoami"],
|
||||
resolution: {
|
||||
rawExecutable: "/usr/bin/time",
|
||||
resolvedPath: "/usr/bin/time",
|
||||
executableName: "time",
|
||||
},
|
||||
},
|
||||
],
|
||||
cwd: dir,
|
||||
env: makePathEnv(dir),
|
||||
platform: process.platform,
|
||||
});
|
||||
expect(patterns).toEqual([whoami]);
|
||||
expect(patterns).not.toContain("/usr/bin/time");
|
||||
});
|
||||
|
||||
it("unwraps busybox/toybox shell applets and persists inner executables", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
@@ -425,6 +451,23 @@ describe("resolveAllowAlwaysPatterns", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("prevents allow-always bypass for time wrapper chains", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const dir = makeTempDir();
|
||||
const echo = makeExecutable(dir, "echo");
|
||||
makeExecutable(dir, "id");
|
||||
const env = makePathEnv(dir);
|
||||
expectAllowAlwaysBypassBlocked({
|
||||
dir,
|
||||
firstCommand: "/usr/bin/time -p /bin/zsh -lc 'echo warmup-ok'",
|
||||
secondCommand: "/usr/bin/time -p /bin/zsh -lc 'id > marker'",
|
||||
env,
|
||||
persistedPattern: echo,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not persist comment-tailed payload paths that never execute", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
|
||||
@@ -144,6 +144,14 @@ describe("exec-command-resolution", () => {
|
||||
]);
|
||||
expect(niceResolution?.rawExecutable).toBe("bash");
|
||||
expect(niceResolution?.executableName.toLowerCase()).toContain("bash");
|
||||
|
||||
const timeResolution = resolveCommandResolutionFromArgv(
|
||||
["/usr/bin/time", "-p", "rg", "-n", "needle"],
|
||||
undefined,
|
||||
makePathEnv(fixture.binDir),
|
||||
);
|
||||
expect(timeResolution?.resolvedPath).toBe(fixture.exePath);
|
||||
expect(timeResolution?.executableName).toBe(fixture.exeName);
|
||||
});
|
||||
|
||||
it("blocks semantic env wrappers, env -S, and deep transparent-wrapper chains", () => {
|
||||
|
||||
@@ -24,6 +24,7 @@ const DISPATCH_WRAPPER_NAMES = [
|
||||
"stdbuf",
|
||||
"sudo",
|
||||
"taskset",
|
||||
"time",
|
||||
"timeout",
|
||||
] as const;
|
||||
|
||||
@@ -86,9 +87,24 @@ const ENV_INLINE_VALUE_PREFIXES = [
|
||||
const ENV_FLAG_OPTIONS = new Set(["-i", "--ignore-environment", "-0", "--null"]);
|
||||
const NICE_OPTIONS_WITH_VALUE = new Set(["-n", "--adjustment", "--priority"]);
|
||||
const STDBUF_OPTIONS_WITH_VALUE = new Set(["-i", "--input", "-o", "--output", "-e", "--error"]);
|
||||
const TIME_FLAG_OPTIONS = new Set([
|
||||
"-a",
|
||||
"--append",
|
||||
"-h",
|
||||
"--help",
|
||||
"-l",
|
||||
"-p",
|
||||
"-q",
|
||||
"--quiet",
|
||||
"-v",
|
||||
"--verbose",
|
||||
"-V",
|
||||
"--version",
|
||||
]);
|
||||
const TIME_OPTIONS_WITH_VALUE = new Set(["-f", "--format", "-o", "--output"]);
|
||||
const TIMEOUT_FLAG_OPTIONS = new Set(["--foreground", "--preserve-status", "-v", "--verbose"]);
|
||||
const TIMEOUT_OPTIONS_WITH_VALUE = new Set(["-k", "--kill-after", "-s", "--signal"]);
|
||||
const TRANSPARENT_DISPATCH_WRAPPERS = new Set(["nice", "nohup", "stdbuf", "timeout"]);
|
||||
const TRANSPARENT_DISPATCH_WRAPPERS = new Set(["nice", "nohup", "stdbuf", "time", "timeout"]);
|
||||
|
||||
type ShellWrapperKind = "posix" | "cmd" | "powershell";
|
||||
|
||||
@@ -371,6 +387,20 @@ function unwrapStdbufInvocation(argv: string[]): string[] | null {
|
||||
});
|
||||
}
|
||||
|
||||
function unwrapTimeInvocation(argv: string[]): string[] | null {
|
||||
return unwrapDashOptionInvocation(argv, {
|
||||
onFlag: (flag, lower) => {
|
||||
if (TIME_FLAG_OPTIONS.has(flag)) {
|
||||
return "continue";
|
||||
}
|
||||
if (TIME_OPTIONS_WITH_VALUE.has(flag)) {
|
||||
return lower.includes("=") ? "continue" : "consume-next";
|
||||
}
|
||||
return "invalid";
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function unwrapTimeoutInvocation(argv: string[]): string[] | null {
|
||||
return unwrapDashOptionInvocation(argv, {
|
||||
onFlag: (flag, lower) => {
|
||||
@@ -430,6 +460,8 @@ export function unwrapKnownDispatchWrapperInvocation(argv: string[]): DispatchWr
|
||||
return unwrapDispatchWrapper(wrapper, unwrapNohupInvocation(argv));
|
||||
case "stdbuf":
|
||||
return unwrapDispatchWrapper(wrapper, unwrapStdbufInvocation(argv));
|
||||
case "time":
|
||||
return unwrapDispatchWrapper(wrapper, unwrapTimeInvocation(argv));
|
||||
case "timeout":
|
||||
return unwrapDispatchWrapper(wrapper, unwrapTimeoutInvocation(argv));
|
||||
case "chrt":
|
||||
|
||||
Reference in New Issue
Block a user