fix(exec): keep awk and sed out of safeBins fast path (#58175)

* wip(exec): preserve safe-bin semantics progress

* test(exec): cover safe-bin semantic variants

* fix(exec): address safe-bin review follow-up
This commit is contained in:
Vincent Koc 2026-03-31 19:29:53 +09:00 committed by GitHub
parent 330a9f98cb
commit 57fccca2dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 177 additions and 4 deletions

View File

@ -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.

View File

@ -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");

View File

@ -27,6 +27,7 @@ describe("exec approvals safe bins", () => {
resolvedPath: string;
expected: boolean;
safeBins?: string[];
safeBinProfiles?: Readonly<Record<string, { minPositional?: number; maxPositional?: number }>>;
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);
});

View File

@ -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", () => {

View File

@ -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",

View File

@ -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.",
},
]);
});
});

View File

@ -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<Record<string, SafeBinSemanticRule>> = {
jq: {
@ -17,8 +24,31 @@ const SAFE_BIN_SEMANTIC_RULES: Readonly<Record<string, SafeBinSemanticRule>> = {
!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,
},
};