mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 22:31:25 +00:00
fix(security): block safeBins shell expansion
This commit is contained in:
@@ -338,6 +338,9 @@ export function emitExecSystemEvent(
|
||||
|
||||
export async function runExecProcess(opts: {
|
||||
command: string;
|
||||
// Execute this instead of `command` (which is kept for display/session/logging).
|
||||
// Used to sanitize safeBins execution while preserving the original user input.
|
||||
execCommand?: string;
|
||||
workdir: string;
|
||||
env: Record<string, string>;
|
||||
sandbox?: BashSandboxConfig;
|
||||
@@ -357,6 +360,7 @@ export async function runExecProcess(opts: {
|
||||
let child: ChildProcessWithoutNullStreams | null = null;
|
||||
let pty: PtyHandle | null = null;
|
||||
let stdin: SessionStdin | undefined;
|
||||
const execCommand = opts.execCommand ?? opts.command;
|
||||
|
||||
if (opts.sandbox) {
|
||||
const { child: spawned } = await spawnWithFallback({
|
||||
@@ -364,7 +368,7 @@ export async function runExecProcess(opts: {
|
||||
"docker",
|
||||
...buildDockerExecArgs({
|
||||
containerName: opts.sandbox.containerName,
|
||||
command: opts.command,
|
||||
command: execCommand,
|
||||
workdir: opts.containerWorkdir ?? opts.sandbox.containerWorkdir,
|
||||
env: opts.env,
|
||||
tty: opts.usePty,
|
||||
@@ -403,7 +407,7 @@ export async function runExecProcess(opts: {
|
||||
if (!spawnPty) {
|
||||
throw new Error("PTY support is unavailable (node-pty spawn not found).");
|
||||
}
|
||||
pty = spawnPty(shell, [...shellArgs, opts.command], {
|
||||
pty = spawnPty(shell, [...shellArgs, execCommand], {
|
||||
cwd: opts.workdir,
|
||||
env: opts.env,
|
||||
name: process.env.TERM ?? "xterm-256color",
|
||||
@@ -435,7 +439,7 @@ export async function runExecProcess(opts: {
|
||||
logWarn(`exec: PTY spawn failed (${errText}); retrying without PTY for "${opts.command}".`);
|
||||
opts.warnings.push(warning);
|
||||
const { child: spawned } = await spawnWithFallback({
|
||||
argv: [shell, ...shellArgs, opts.command],
|
||||
argv: [shell, ...shellArgs, execCommand],
|
||||
options: {
|
||||
cwd: opts.workdir,
|
||||
env: opts.env,
|
||||
@@ -462,7 +466,7 @@ export async function runExecProcess(opts: {
|
||||
} else {
|
||||
const { shell, args: shellArgs } = getShellConfig();
|
||||
const { child: spawned } = await spawnWithFallback({
|
||||
argv: [shell, ...shellArgs, opts.command],
|
||||
argv: [shell, ...shellArgs, execCommand],
|
||||
options: {
|
||||
cwd: opts.workdir,
|
||||
env: opts.env,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
recordAllowlistUse,
|
||||
resolveExecApprovals,
|
||||
resolveExecApprovalsFromFile,
|
||||
buildSafeShellCommand,
|
||||
} from "../infra/exec-approvals.js";
|
||||
import { buildNodeShellCommand } from "../infra/node-shell.js";
|
||||
import {
|
||||
@@ -170,6 +171,7 @@ export function createExecTool(
|
||||
const maxOutput = DEFAULT_MAX_OUTPUT;
|
||||
const pendingMaxOutput = DEFAULT_PENDING_MAX_OUTPUT;
|
||||
const warnings: string[] = [];
|
||||
let execCommandOverride: string | undefined;
|
||||
const backgroundRequested = params.background === true;
|
||||
const yieldRequested = typeof params.yieldMs === "number";
|
||||
if (!allowBackground && (backgroundRequested || yieldRequested)) {
|
||||
@@ -804,6 +806,25 @@ export function createExecTool(
|
||||
throw new Error("exec denied: allowlist miss");
|
||||
}
|
||||
|
||||
// If allowlist is satisfied only via safeBins (no explicit allowlist match),
|
||||
// run a sanitized `shell -c` command that disables glob/var expansion by
|
||||
// forcing every argv token to be literal via single-quoting.
|
||||
if (
|
||||
hostSecurity === "allowlist" &&
|
||||
analysisOk &&
|
||||
allowlistSatisfied &&
|
||||
allowlistMatches.length === 0
|
||||
) {
|
||||
const safe = buildSafeShellCommand({
|
||||
command: params.command,
|
||||
platform: process.platform,
|
||||
});
|
||||
if (!safe.ok || !safe.command) {
|
||||
throw new Error(`exec denied: safeBins sanitize failed (${safe.reason ?? "unknown"})`);
|
||||
}
|
||||
execCommandOverride = safe.command;
|
||||
}
|
||||
|
||||
if (allowlistMatches.length > 0) {
|
||||
const seen = new Set<string>();
|
||||
for (const match of allowlistMatches) {
|
||||
@@ -828,6 +849,7 @@ export function createExecTool(
|
||||
const usePty = params.pty === true && !sandbox;
|
||||
const run = await runExecProcess({
|
||||
command: params.command,
|
||||
execCommand: execCommandOverride,
|
||||
workdir,
|
||||
env,
|
||||
sandbox,
|
||||
|
||||
@@ -130,4 +130,46 @@ describe("createOpenClawCodingTools safeBins", () => {
|
||||
expect(result.details.status).toBe("completed");
|
||||
expect(text).toContain(marker);
|
||||
});
|
||||
|
||||
it("does not allow env var expansion to smuggle file args via safeBins", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
|
||||
const { createOpenClawCodingTools } = await import("./pi-tools.js");
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-safe-bins-expand-"));
|
||||
|
||||
const secret = `TOP_SECRET_${Date.now()}`;
|
||||
fs.writeFileSync(path.join(tmpDir, "secret.txt"), `${secret}\n`, "utf8");
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
tools: {
|
||||
exec: {
|
||||
host: "gateway",
|
||||
security: "allowlist",
|
||||
ask: "off",
|
||||
safeBins: ["head", "wc"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const tools = createOpenClawCodingTools({
|
||||
config: cfg,
|
||||
sessionKey: "agent:main:main",
|
||||
workspaceDir: tmpDir,
|
||||
agentDir: path.join(tmpDir, "agent"),
|
||||
});
|
||||
const execTool = tools.find((tool) => tool.name === "exec");
|
||||
expect(execTool).toBeDefined();
|
||||
|
||||
const result = await execTool!.execute("call1", {
|
||||
command: "head $FOO ; wc -l",
|
||||
workdir: tmpDir,
|
||||
env: { FOO: "secret.txt" },
|
||||
});
|
||||
const text = result.content.find((content) => content.type === "text")?.text ?? "";
|
||||
|
||||
expect(result.details.status).toBe("completed");
|
||||
expect(text).not.toContain(secret);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user