diff --git a/CHANGELOG.md b/CHANGELOG.md index 009c058cb13..3d2d8c62b99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,7 @@ Docs: https://docs.openclaw.ai - Docker/setup: force BuildKit for local image builds (including sandbox image builds) so `./docker-setup.sh` no longer fails on `RUN --mount=...` when hosts default to Docker's legacy builder. (#56681) Thanks @zhanghui-china. - Control UI/agents: auto-load agent workspace files on initial Files panel open, and populate overview model/workspace/fallbacks from effective runtime agent metadata so defaulted models no longer show as `Not set`. (#56637) Thanks @dxsx84. - Control UI/slash commands: make `/steer` and `/redirect` work from the chat command palette with visible pending state for active-run `/steer`, correct redirected-run tracking, and a single canonical `/steer` entry in the command menu. (#54625) Thanks @fuller-stack-dev. +- Exec/approvals: keep `awk` and `sed` family binaries out of the low-risk `safeBins` fast path, and stop doctor profile scaffolding from treating them like ordinary custom filters. Thanks @vincentkoc. - Exec/runtime: default implicit exec to `host=auto`, resolve that target to sandbox only when a sandbox runtime exists, keep explicit `host=sandbox` fail-closed without sandbox, and show `/exec` effective host state in runtime status/docs. - Exec: fail closed when the implicit sandbox host has no sandbox runtime, and stop denied async approval followups from reusing prior command output from the same session. (#56800) Thanks @scoootscooob. - Exec/approvals: infer Discord and Telegram exec approvers from existing owner config when `execApprovals.approvers` is unset, extend the default approval window to 30 minutes, and clarify approval-unavailable guidance so approvals do not appear to silently disappear. diff --git a/src/commands/doctor/shared/exec-safe-bins.test.ts b/src/commands/doctor/shared/exec-safe-bins.test.ts index 0dfe7058cf3..2e08326b924 100644 --- a/src/commands/doctor/shared/exec-safe-bins.test.ts +++ b/src/commands/doctor/shared/exec-safe-bins.test.ts @@ -81,6 +81,25 @@ describe("doctor exec safe bin helpers", () => { expect(result.config.tools?.exec?.safeBinProfiles).toEqual({ jq: {} }); }); + it("warns on awk-family safeBins instead of scaffolding them", () => { + const result = maybeRepairExecSafeBinProfiles({ + tools: { + exec: { + safeBins: ["awk", "sed"], + }, + }, + } as OpenClawConfig); + + expect(result.changes).toEqual([]); + expect(result.warnings).toEqual([ + "- tools.exec.safeBins includes 'awk': awk-family interpreters can execute commands, access ENVIRON, and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.", + "- tools.exec.safeBins includes 'sed': sed scripts can execute commands and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.", + "- tools.exec.safeBins includes interpreter/runtime 'awk' without profile; remove it from safeBins or use explicit allowlist entries.", + "- tools.exec.safeBins includes interpreter/runtime 'sed' without profile; remove it from safeBins or use explicit allowlist entries.", + ]); + expect(result.config.tools?.exec?.safeBinProfiles).toEqual({}); + }); + it("flags safeBins that resolve outside trusted directories", () => { const tempDir = mkdtempSync(join(tmpdir(), "openclaw-safe-bin-")); const binPath = join(tempDir, "custom-safe-bin"); diff --git a/src/infra/exec-approvals-safe-bins.test.ts b/src/infra/exec-approvals-safe-bins.test.ts index 2ff18747b6e..983a04b279e 100644 --- a/src/infra/exec-approvals-safe-bins.test.ts +++ b/src/infra/exec-approvals-safe-bins.test.ts @@ -27,6 +27,7 @@ describe("exec approvals safe bins", () => { resolvedPath: string; expected: boolean; safeBins?: string[]; + safeBinProfiles?: Readonly>; executableName?: string; rawExecutable?: string; cwd?: string; @@ -197,6 +198,24 @@ describe("exec approvals safe bins", () => { resolvedPath: "/usr/bin/jq", expected: false, }, + { + name: "blocks awk scripts even when awk is explicitly profiled", + argv: ["awk", 'BEGIN { system("id") }'], + resolvedPath: "/usr/bin/awk", + expected: false, + safeBins: ["awk"], + safeBinProfiles: { awk: {} }, + executableName: "awk", + }, + { + name: "blocks sed scripts even when sed is explicitly profiled", + argv: ["sed", "e"], + resolvedPath: "/usr/bin/sed", + expected: false, + safeBins: ["sed"], + safeBinProfiles: { sed: {} }, + executableName: "sed", + }, { name: "blocks safe bins with file args", argv: ["jq", ".foo", "secret.json"], @@ -267,6 +286,7 @@ describe("exec approvals safe bins", () => { executableName, }, safeBins: normalizeSafeBins(testCase.safeBins ?? [executableName]), + safeBinProfiles: testCase.safeBinProfiles, }); expect(ok).toBe(testCase.expected); }); diff --git a/src/infra/exec-safe-bin-runtime-policy.test.ts b/src/infra/exec-safe-bin-runtime-policy.test.ts index 8bc40b2d35a..40a1c30e3cd 100644 --- a/src/infra/exec-safe-bin-runtime-policy.test.ts +++ b/src/infra/exec-safe-bin-runtime-policy.test.ts @@ -17,6 +17,12 @@ describe("exec safe-bin runtime policy", () => { { bin: "node", expected: true }, { bin: "node20", expected: true }, { bin: "/usr/local/bin/node20", expected: true }, + { bin: "awk", expected: true }, + { bin: "/opt/homebrew/bin/gawk", expected: true }, + { bin: "mawk", expected: true }, + { bin: "nawk", expected: true }, + { bin: "sed", expected: true }, + { bin: "gsed", expected: true }, { bin: "ruby3.2", expected: true }, { bin: "bash", expected: true }, { bin: "busybox", expected: true }, @@ -33,8 +39,14 @@ describe("exec safe-bin runtime policy", () => { it("lists interpreter-like bins from a mixed set", () => { expect( - listInterpreterLikeSafeBins(["jq", " C:\\Tools\\Python3.EXE ", "myfilter", "/usr/bin/node"]), - ).toEqual(["node", "python3"]); + listInterpreterLikeSafeBins([ + "jq", + " C:\\Tools\\Python3.EXE ", + "myfilter", + "/usr/bin/node", + "/opt/homebrew/bin/gawk", + ]), + ).toEqual(["gawk", "node", "python3"]); }); it("merges and normalizes safe-bin profile fixtures", () => { diff --git a/src/infra/exec-safe-bin-runtime-policy.ts b/src/infra/exec-safe-bin-runtime-policy.ts index 4a483040dd2..af60fe0a71e 100644 --- a/src/infra/exec-safe-bin-runtime-policy.ts +++ b/src/infra/exec-safe-bin-runtime-policy.ts @@ -22,6 +22,7 @@ export type ExecSafeBinConfigScope = { const INTERPRETER_LIKE_SAFE_BINS = new Set([ "ash", + "awk", "bash", "busybox", "bun", @@ -31,8 +32,12 @@ const INTERPRETER_LIKE_SAFE_BINS = new Set([ "dash", "deno", "fish", + "gawk", + "gsed", "ksh", "lua", + "mawk", + "nawk", "node", "nodejs", "perl", @@ -46,6 +51,7 @@ const INTERPRETER_LIKE_SAFE_BINS = new Set([ "python2", "python3", "ruby", + "sed", "sh", "toybox", "wscript", diff --git a/src/infra/exec-safe-bin-semantics.test.ts b/src/infra/exec-safe-bin-semantics.test.ts new file mode 100644 index 00000000000..9e575e50b7b --- /dev/null +++ b/src/infra/exec-safe-bin-semantics.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from "vitest"; +import { + listRiskyConfiguredSafeBins, + validateSafeBinSemantics, +} from "./exec-safe-bin-semantics.js"; + +describe("exec safe-bin semantics", () => { + it("rejects awk and sed variants even when configured via path-like entries", () => { + expect( + validateSafeBinSemantics({ + binName: "/opt/homebrew/bin/gawk", + positional: ['BEGIN { system("id") }'], + }), + ).toBe(false); + expect( + validateSafeBinSemantics({ + binName: "C:\\Tools\\mawk.exe", + positional: ['BEGIN { print ENVIRON["HOME"] }'], + }), + ).toBe(false); + expect( + validateSafeBinSemantics({ + binName: "nawk", + positional: ['BEGIN { print "hi" > "/tmp/out" }'], + }), + ).toBe(false); + expect( + validateSafeBinSemantics({ + binName: "/usr/local/bin/gsed", + positional: ["e"], + }), + ).toBe(false); + }); + + it("reports normalized risky configured safe bins once per executable family member", () => { + expect( + listRiskyConfiguredSafeBins([ + " Awk ", + "/opt/homebrew/bin/gawk", + "C:\\Tools\\mawk.exe", + "nawk", + "sed", + "/usr/local/bin/gsed", + "jq", + "jq", + ]), + ).toEqual([ + { + bin: "awk", + warning: + "awk-family interpreters can execute commands, access ENVIRON, and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.", + }, + { + bin: "gawk", + warning: + "awk-family interpreters can execute commands, access ENVIRON, and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.", + }, + { + bin: "gsed", + warning: + "sed scripts can execute commands and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.", + }, + { + bin: "jq", + warning: + "jq supports broad jq programs and builtins (for example `env`), so prefer explicit allowlist entries or approval-gated runs instead of safeBins.", + }, + { + bin: "mawk", + warning: + "awk-family interpreters can execute commands, access ENVIRON, and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.", + }, + { + bin: "nawk", + warning: + "awk-family interpreters can execute commands, access ENVIRON, and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.", + }, + { + bin: "sed", + warning: + "sed scripts can execute commands and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.", + }, + ]); + }); +}); diff --git a/src/infra/exec-safe-bin-semantics.ts b/src/infra/exec-safe-bin-semantics.ts index 0c2e71caa11..eda28d2380c 100644 --- a/src/infra/exec-safe-bin-semantics.ts +++ b/src/infra/exec-safe-bin-semantics.ts @@ -10,6 +10,13 @@ type SafeBinSemanticRule = { const JQ_ENV_FILTER_PATTERN = /(^|[^.$A-Za-z0-9_])env([^A-Za-z0-9_]|$)/; const JQ_ENV_VARIABLE_PATTERN = /\$ENV\b/; +const ALWAYS_DENY_SAFE_BIN_SEMANTICS = () => false; + +const UNSAFE_SAFE_BIN_WARNINGS = { + awk: "awk-family interpreters can execute commands, access ENVIRON, and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.", + jq: "jq supports broad jq programs and builtins (for example `env`), so prefer explicit allowlist entries or approval-gated runs instead of safeBins.", + sed: "sed scripts can execute commands and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.", +} as const; const SAFE_BIN_SEMANTIC_RULES: Readonly> = { jq: { @@ -17,8 +24,31 @@ const SAFE_BIN_SEMANTIC_RULES: Readonly> = { !positional.some( (token) => JQ_ENV_FILTER_PATTERN.test(token) || JQ_ENV_VARIABLE_PATTERN.test(token), ), - configWarning: - "jq supports broad jq programs and builtins (for example `env`), so prefer explicit allowlist entries or approval-gated runs instead of safeBins.", + configWarning: UNSAFE_SAFE_BIN_WARNINGS.jq, + }, + awk: { + validate: ALWAYS_DENY_SAFE_BIN_SEMANTICS, + configWarning: UNSAFE_SAFE_BIN_WARNINGS.awk, + }, + gawk: { + validate: ALWAYS_DENY_SAFE_BIN_SEMANTICS, + configWarning: UNSAFE_SAFE_BIN_WARNINGS.awk, + }, + mawk: { + validate: ALWAYS_DENY_SAFE_BIN_SEMANTICS, + configWarning: UNSAFE_SAFE_BIN_WARNINGS.awk, + }, + nawk: { + validate: ALWAYS_DENY_SAFE_BIN_SEMANTICS, + configWarning: UNSAFE_SAFE_BIN_WARNINGS.awk, + }, + sed: { + validate: ALWAYS_DENY_SAFE_BIN_SEMANTICS, + configWarning: UNSAFE_SAFE_BIN_WARNINGS.sed, + }, + gsed: { + validate: ALWAYS_DENY_SAFE_BIN_SEMANTICS, + configWarning: UNSAFE_SAFE_BIN_WARNINGS.sed, }, };