mirror of https://github.com/openclaw/openclaw.git
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:
parent
330a9f98cb
commit
57fccca2dc
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue