mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-25 17:02:46 +00:00
Infra: preserve wrapper executable for multiplexer trust
This commit is contained in:
committed by
Peter Steinberger
parent
2d5f822ca1
commit
32e89b4687
@@ -154,7 +154,7 @@ describe("exec-command-resolution", () => {
|
||||
expect(timeResolution?.executableName).toBe(fixture.exeName);
|
||||
});
|
||||
|
||||
it("unwraps shell multiplexers before resolving the effective executable", () => {
|
||||
it("keeps shell multiplexer wrappers as the trusted executable target", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
@@ -164,9 +164,43 @@ describe("exec-command-resolution", () => {
|
||||
fs.chmodSync(busybox, 0o755);
|
||||
|
||||
const resolution = resolveCommandResolutionFromArgv([busybox, "sh", "-lc", "echo hi"]);
|
||||
expect(resolution?.rawExecutable).toBe("sh");
|
||||
expect(resolution?.rawExecutable).toBe(busybox);
|
||||
expect(resolution?.effectiveArgv).toEqual(["sh", "-lc", "echo hi"]);
|
||||
expect(resolution?.wrapperChain).toEqual(["busybox"]);
|
||||
expect(resolution?.executableName.toLowerCase()).toContain("sh");
|
||||
expect(resolution?.resolvedPath).toBe(busybox);
|
||||
expect(resolution?.executableName.toLowerCase()).toContain("busybox");
|
||||
});
|
||||
|
||||
it("does not satisfy inner-shell allowlists when invoked through busybox wrappers", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const dir = makeTempDir();
|
||||
const busybox = path.join(dir, "busybox");
|
||||
fs.writeFileSync(busybox, "");
|
||||
fs.chmodSync(busybox, 0o755);
|
||||
|
||||
const shellResolution = resolveCommandResolutionFromArgv(["sh", "-lc", "echo hi"]);
|
||||
expect(shellResolution?.resolvedPath).toBeTruthy();
|
||||
|
||||
const wrappedResolution = resolveCommandResolutionFromArgv([busybox, "sh", "-lc", "echo hi"]);
|
||||
const evalResult = evaluateExecAllowlist({
|
||||
analysis: {
|
||||
ok: true,
|
||||
segments: [
|
||||
{
|
||||
raw: `${busybox} sh -lc echo hi`,
|
||||
argv: [busybox, "sh", "-lc", "echo hi"],
|
||||
resolution: wrappedResolution,
|
||||
},
|
||||
],
|
||||
},
|
||||
allowlist: [{ pattern: shellResolution?.resolvedPath ?? "" }],
|
||||
safeBins: normalizeSafeBins([]),
|
||||
cwd: dir,
|
||||
});
|
||||
|
||||
expect(evalResult.allowlistSatisfied).toBe(false);
|
||||
});
|
||||
|
||||
it("blocks semantic env wrappers, env -S, and deep transparent-wrapper chains", () => {
|
||||
|
||||
@@ -98,7 +98,8 @@ export function resolveCommandResolutionFromArgv(
|
||||
): CommandResolution | null {
|
||||
const plan = resolveExecWrapperTrustPlan(argv);
|
||||
const effectiveArgv = plan.argv;
|
||||
const rawExecutable = effectiveArgv[0]?.trim();
|
||||
const policyArgv = plan.policyArgv;
|
||||
const rawExecutable = policyArgv[0]?.trim();
|
||||
if (!rawExecutable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ describe("resolveExecWrapperTrustPlan", () => {
|
||||
resolveExecWrapperTrustPlan(["/usr/bin/time", "-p", "busybox", "sh", "-lc", "echo hi"]),
|
||||
).toEqual({
|
||||
argv: ["sh", "-lc", "echo hi"],
|
||||
policyArgv: ["busybox", "sh", "-lc", "echo hi"],
|
||||
wrapperChain: ["time", "busybox"],
|
||||
policyBlocked: false,
|
||||
shellWrapperExecutable: true,
|
||||
@@ -20,6 +21,7 @@ describe("resolveExecWrapperTrustPlan", () => {
|
||||
test("fails closed for unsupported shell multiplexer applets", () => {
|
||||
expect(resolveExecWrapperTrustPlan(["busybox", "sed", "-n", "1p"])).toEqual({
|
||||
argv: ["busybox", "sed", "-n", "1p"],
|
||||
policyArgv: ["busybox", "sed", "-n", "1p"],
|
||||
wrapperChain: [],
|
||||
policyBlocked: true,
|
||||
blockedWrapper: "busybox",
|
||||
@@ -33,6 +35,7 @@ describe("resolveExecWrapperTrustPlan", () => {
|
||||
resolveExecWrapperTrustPlan(["nohup", "timeout", "5s", "busybox", "sh", "-lc", "echo hi"], 2),
|
||||
).toEqual({
|
||||
argv: ["busybox", "sh", "-lc", "echo hi"],
|
||||
policyArgv: ["busybox", "sh", "-lc", "echo hi"],
|
||||
wrapperChain: ["nohup", "timeout"],
|
||||
policyBlocked: true,
|
||||
blockedWrapper: "busybox",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
|
||||
export type ExecWrapperTrustPlan = {
|
||||
argv: string[];
|
||||
policyArgv: string[];
|
||||
wrapperChain: string[];
|
||||
policyBlocked: boolean;
|
||||
blockedWrapper?: string;
|
||||
@@ -20,11 +21,13 @@ export type ExecWrapperTrustPlan = {
|
||||
|
||||
function blockedExecWrapperTrustPlan(params: {
|
||||
argv: string[];
|
||||
policyArgv?: string[];
|
||||
wrapperChain: string[];
|
||||
blockedWrapper: string;
|
||||
}): ExecWrapperTrustPlan {
|
||||
return {
|
||||
argv: params.argv,
|
||||
policyArgv: params.policyArgv ?? params.argv,
|
||||
wrapperChain: params.wrapperChain,
|
||||
policyBlocked: true,
|
||||
blockedWrapper: params.blockedWrapper,
|
||||
@@ -35,6 +38,7 @@ function blockedExecWrapperTrustPlan(params: {
|
||||
|
||||
function finalizeExecWrapperTrustPlan(
|
||||
argv: string[],
|
||||
policyArgv: string[],
|
||||
wrapperChain: string[],
|
||||
policyBlocked: boolean,
|
||||
blockedWrapper?: string,
|
||||
@@ -44,6 +48,7 @@ function finalizeExecWrapperTrustPlan(
|
||||
!policyBlocked && rawExecutable.length > 0 && isShellWrapperExecutable(rawExecutable);
|
||||
return {
|
||||
argv,
|
||||
policyArgv,
|
||||
wrapperChain,
|
||||
policyBlocked,
|
||||
blockedWrapper,
|
||||
@@ -57,12 +62,15 @@ export function resolveExecWrapperTrustPlan(
|
||||
maxDepth = MAX_DISPATCH_WRAPPER_DEPTH,
|
||||
): ExecWrapperTrustPlan {
|
||||
let current = argv;
|
||||
let policyArgv = argv;
|
||||
let sawShellMultiplexer = false;
|
||||
const wrapperChain: string[] = [];
|
||||
for (let depth = 0; depth < maxDepth; depth += 1) {
|
||||
const dispatchPlan = resolveDispatchWrapperTrustPlan(current, maxDepth - wrapperChain.length);
|
||||
if (dispatchPlan.policyBlocked) {
|
||||
return blockedExecWrapperTrustPlan({
|
||||
argv: dispatchPlan.argv,
|
||||
policyArgv,
|
||||
wrapperChain,
|
||||
blockedWrapper: dispatchPlan.blockedWrapper ?? current[0] ?? "unknown",
|
||||
});
|
||||
@@ -70,6 +78,9 @@ export function resolveExecWrapperTrustPlan(
|
||||
if (dispatchPlan.wrappers.length > 0) {
|
||||
wrapperChain.push(...dispatchPlan.wrappers);
|
||||
current = dispatchPlan.argv;
|
||||
if (!sawShellMultiplexer) {
|
||||
policyArgv = current;
|
||||
}
|
||||
if (wrapperChain.length >= maxDepth) {
|
||||
break;
|
||||
}
|
||||
@@ -80,12 +91,18 @@ export function resolveExecWrapperTrustPlan(
|
||||
if (shellMultiplexerUnwrap.kind === "blocked") {
|
||||
return blockedExecWrapperTrustPlan({
|
||||
argv: current,
|
||||
policyArgv,
|
||||
wrapperChain,
|
||||
blockedWrapper: shellMultiplexerUnwrap.wrapper,
|
||||
});
|
||||
}
|
||||
if (shellMultiplexerUnwrap.kind === "unwrapped") {
|
||||
wrapperChain.push(shellMultiplexerUnwrap.wrapper);
|
||||
if (!sawShellMultiplexer) {
|
||||
// Preserve the real executable target for trust checks.
|
||||
policyArgv = current;
|
||||
sawShellMultiplexer = true;
|
||||
}
|
||||
current = shellMultiplexerUnwrap.argv;
|
||||
if (wrapperChain.length >= maxDepth) {
|
||||
break;
|
||||
@@ -101,6 +118,7 @@ export function resolveExecWrapperTrustPlan(
|
||||
if (dispatchOverflow.kind === "blocked" || dispatchOverflow.kind === "unwrapped") {
|
||||
return blockedExecWrapperTrustPlan({
|
||||
argv: current,
|
||||
policyArgv,
|
||||
wrapperChain,
|
||||
blockedWrapper: dispatchOverflow.wrapper,
|
||||
});
|
||||
@@ -112,11 +130,12 @@ export function resolveExecWrapperTrustPlan(
|
||||
) {
|
||||
return blockedExecWrapperTrustPlan({
|
||||
argv: current,
|
||||
policyArgv,
|
||||
wrapperChain,
|
||||
blockedWrapper: shellMultiplexerOverflow.wrapper,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return finalizeExecWrapperTrustPlan(current, wrapperChain, false);
|
||||
return finalizeExecWrapperTrustPlan(current, policyArgv, wrapperChain, false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user