mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(exec): block shell-wrapper positional argv approval smuggling
This commit is contained in:
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Security/Native images: enforce `tools.fs.workspaceOnly` for native prompt image auto-load (including history refs), preventing out-of-workspace sandbox mounts from being implicitly ingested as vision input. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
- Security/Exec approvals: bind `system.run` command display/approval text to full argv when shell-wrapper inline payloads carry positional argv values, and reject payload-only `rawCommand` mismatches for those wrapper-carrier forms, preventing hidden command execution under misleading approval text. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
- Telegram/Media fetch: prioritize IPv4 before IPv6 in SSRF pinned DNS address ordering so media downloads still work on hosts with broken IPv6 routing. (#24295, #23975) Thanks @Glucksberg.
|
||||
- Sessions/Tool-result guard: avoid generating synthetic `toolResult` entries for assistant turns that ended with `stopReason: "aborted"` or `"error"`, preventing orphaned tool-use IDs from triggering downstream API validation errors. (#25429) Thanks @mikaeldiakhate-cell.
|
||||
- Usage accounting: parse Moonshot/Kimi `cached_tokens` fields (including `prompt_tokens_details.cached_tokens`) into normalized cache-read usage metrics. (#25436) Thanks @Elarwei001.
|
||||
|
||||
@@ -103,6 +103,13 @@ describe("system run command helpers", () => {
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
test("validateSystemRunCommandConsistency rejects shell-only rawCommand for positional-argv carrier wrappers", () => {
|
||||
expectRawCommandMismatch({
|
||||
argv: ["/bin/sh", "-lc", '$0 "$1"', "/usr/bin/touch", "/tmp/marker"],
|
||||
rawCommand: '$0 "$1"',
|
||||
});
|
||||
});
|
||||
|
||||
test("validateSystemRunCommandConsistency accepts rawCommand matching env shell wrapper argv", () => {
|
||||
const res = validateSystemRunCommandConsistency({
|
||||
argv: ["/usr/bin/env", "bash", "-lc", "echo hi"],
|
||||
@@ -170,6 +177,18 @@ describe("system run command helpers", () => {
|
||||
expect(res.cmdText).toBe("echo SAFE&&whoami");
|
||||
});
|
||||
|
||||
test("resolveSystemRunCommand binds cmdText to full argv for shell-wrapper positional-argv carriers", () => {
|
||||
const res = resolveSystemRunCommand({
|
||||
command: ["/bin/sh", "-lc", '$0 "$1"', "/usr/bin/touch", "/tmp/marker"],
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
if (!res.ok) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
expect(res.shellCommand).toBe('$0 "$1"');
|
||||
expect(res.cmdText).toBe('/bin/sh -lc "$0 \\"$1\\"" /usr/bin/touch /tmp/marker');
|
||||
});
|
||||
|
||||
test("resolveSystemRunCommand binds cmdText to full argv when env prelude modifies shell wrapper", () => {
|
||||
const res = resolveSystemRunCommand({
|
||||
command: ["/usr/bin/env", "BASH_ENV=/tmp/payload.sh", "bash", "-lc", "echo hi"],
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import {
|
||||
extractShellWrapperCommand,
|
||||
hasEnvManipulationBeforeShellWrapper,
|
||||
normalizeExecutableToken,
|
||||
unwrapDispatchWrappersForResolution,
|
||||
unwrapKnownShellMultiplexerInvocation,
|
||||
} from "./exec-wrapper-resolution.js";
|
||||
|
||||
export type SystemRunCommandValidation =
|
||||
@@ -49,6 +52,77 @@ export function extractShellCommandFromArgv(argv: string[]): string | null {
|
||||
return extractShellWrapperCommand(argv).command;
|
||||
}
|
||||
|
||||
const POSIX_OR_POWERSHELL_INLINE_WRAPPER_NAMES = new Set([
|
||||
"ash",
|
||||
"bash",
|
||||
"dash",
|
||||
"fish",
|
||||
"ksh",
|
||||
"powershell",
|
||||
"pwsh",
|
||||
"sh",
|
||||
"zsh",
|
||||
]);
|
||||
|
||||
const POSIX_INLINE_COMMAND_FLAGS = new Set(["-lc", "-c", "--command"]);
|
||||
const POWERSHELL_INLINE_COMMAND_FLAGS = new Set(["-c", "-command", "--command"]);
|
||||
|
||||
function unwrapShellWrapperArgv(argv: string[]): string[] {
|
||||
const dispatchUnwrapped = unwrapDispatchWrappersForResolution(argv);
|
||||
const shellMultiplexer = unwrapKnownShellMultiplexerInvocation(dispatchUnwrapped);
|
||||
return shellMultiplexer.kind === "unwrapped" ? shellMultiplexer.argv : dispatchUnwrapped;
|
||||
}
|
||||
|
||||
function resolveInlineCommandTokenIndex(
|
||||
argv: string[],
|
||||
flags: ReadonlySet<string>,
|
||||
options: { allowCombinedC?: boolean } = {},
|
||||
): number | null {
|
||||
for (let i = 1; i < argv.length; i += 1) {
|
||||
const token = argv[i]?.trim();
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
const lower = token.toLowerCase();
|
||||
if (lower === "--") {
|
||||
break;
|
||||
}
|
||||
if (flags.has(lower)) {
|
||||
return i + 1 < argv.length ? i + 1 : null;
|
||||
}
|
||||
if (options.allowCombinedC && /^-[^-]*c[^-]*$/i.test(token)) {
|
||||
const commandIndex = lower.indexOf("c");
|
||||
const inline = token.slice(commandIndex + 1).trim();
|
||||
return inline ? i : i + 1 < argv.length ? i + 1 : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasTrailingPositionalArgvAfterInlineCommand(argv: string[]): boolean {
|
||||
const wrapperArgv = unwrapShellWrapperArgv(argv);
|
||||
const token0 = wrapperArgv[0]?.trim();
|
||||
if (!token0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const wrapper = normalizeExecutableToken(token0);
|
||||
if (!POSIX_OR_POWERSHELL_INLINE_WRAPPER_NAMES.has(wrapper)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const inlineCommandIndex =
|
||||
wrapper === "powershell" || wrapper === "pwsh"
|
||||
? resolveInlineCommandTokenIndex(wrapperArgv, POWERSHELL_INLINE_COMMAND_FLAGS)
|
||||
: resolveInlineCommandTokenIndex(wrapperArgv, POSIX_INLINE_COMMAND_FLAGS, {
|
||||
allowCombinedC: true,
|
||||
});
|
||||
if (inlineCommandIndex === null) {
|
||||
return false;
|
||||
}
|
||||
return wrapperArgv.slice(inlineCommandIndex + 1).some((entry) => entry.trim().length > 0);
|
||||
}
|
||||
|
||||
export function validateSystemRunCommandConsistency(params: {
|
||||
argv: string[];
|
||||
rawCommand?: string | null;
|
||||
@@ -59,10 +133,12 @@ export function validateSystemRunCommandConsistency(params: {
|
||||
: null;
|
||||
const shellWrapperResolution = extractShellWrapperCommand(params.argv);
|
||||
const shellCommand = shellWrapperResolution.command;
|
||||
const shellWrapperPositionalArgv = hasTrailingPositionalArgvAfterInlineCommand(params.argv);
|
||||
const envManipulationBeforeShellWrapper =
|
||||
shellWrapperResolution.isWrapper && hasEnvManipulationBeforeShellWrapper(params.argv);
|
||||
const mustBindDisplayToFullArgv = envManipulationBeforeShellWrapper || shellWrapperPositionalArgv;
|
||||
const inferred =
|
||||
shellCommand !== null && !envManipulationBeforeShellWrapper
|
||||
shellCommand !== null && !mustBindDisplayToFullArgv
|
||||
? shellCommand.trim()
|
||||
: formatExecCommand(params.argv);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user