mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
Harden jq safe-bin semantics (#102032)
This commit is contained in:
@@ -61,12 +61,11 @@ Denied flags by safe-bin profile:
|
||||
|
||||
Safe bins also force argv tokens to be treated as **literal text** at execution
|
||||
time (no globbing and no `$VARS` expansion) for stdin-only segments, so
|
||||
patterns like `*` or `$HOME/...` cannot be used to smuggle file reads. `awk`
|
||||
and `sed` are always denied as safe bins (their semantics cannot be validated
|
||||
to stdin-only); `jq` can be opted in, but OpenClaw still rejects `env`-style
|
||||
filters (for example `jq env` or `jq -n env`) in safe-bin mode so `jq` cannot
|
||||
dump the host process environment without an explicit allowlist path or
|
||||
approval prompt.
|
||||
patterns like `*` or `$HOME/...` cannot be used to smuggle file reads. `awk`,
|
||||
`sed`, and `jq` are always denied as safe bins because their semantics cannot be
|
||||
validated to stdin-only: `jq` can read environment data and load jq code from
|
||||
modules or startup files. Use an explicit allowlist entry or approval prompt for
|
||||
those tools instead of `safeBins`.
|
||||
|
||||
### Trusted binary directories
|
||||
|
||||
@@ -131,7 +130,7 @@ Custom profile example:
|
||||
{
|
||||
tools: {
|
||||
exec: {
|
||||
safeBins: ["jq", "myfilter"],
|
||||
safeBins: ["myfilter"],
|
||||
safeBinProfiles: {
|
||||
myfilter: {
|
||||
minPositional: 0,
|
||||
|
||||
+1
-1
@@ -189,7 +189,7 @@ Use the two controls for different jobs:
|
||||
|
||||
Do not treat `safeBins` as a generic allowlist, and do not add interpreter/runtime binaries (for example `python3`, `node`, `ruby`, `bash`). If you need those, use explicit allowlist entries and keep approval prompts enabled.
|
||||
|
||||
`openclaw security audit` warns when interpreter/runtime `safeBins` entries are missing explicit profiles, and `openclaw doctor --fix` can scaffold missing custom `safeBinProfiles` entries. `openclaw security audit` and `openclaw doctor` also warn when you explicitly add broad-behavior bins such as `jq` back into `safeBins` (`jq` supports broad programs and builtins, so prefer explicit allowlist entries or approval-gated runs instead). If you explicitly allowlist interpreters, enable `tools.exec.strictInlineEval` so inline code-eval forms still require reviewer or explicit approval.
|
||||
`openclaw security audit` warns when interpreter/runtime `safeBins` entries are missing explicit profiles, and `openclaw doctor --fix` can scaffold missing custom `safeBinProfiles` entries. `openclaw security audit` and `openclaw doctor` also warn when you explicitly add broad-behavior bins such as `jq` back into `safeBins` (`jq` can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead). `jq` is denied as a safe bin even when it is explicitly listed. If you explicitly allowlist interpreters, enable `tools.exec.strictInlineEval` so inline code-eval forms still require reviewer or explicit approval.
|
||||
|
||||
For full policy details and examples, see [Exec approvals](/tools/exec-approvals-advanced#safe-bins-stdin-only) and [Safe bins versus allowlist](/tools/exec-approvals-advanced#safe-bins-versus-allowlist).
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ describe("doctor exec safe bin helpers", () => {
|
||||
bin: "jq",
|
||||
kind: "riskySemantics",
|
||||
warning:
|
||||
"jq supports broad jq programs and builtins (for example `env`), so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
|
||||
"jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -50,7 +50,13 @@ describe("doctor exec safe bin helpers", () => {
|
||||
bin: "jq",
|
||||
kind: "riskySemantics",
|
||||
warning:
|
||||
"jq supports broad jq programs and builtins (for example `env`), so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
|
||||
"jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
|
||||
},
|
||||
{
|
||||
scopePath: "tools.exec",
|
||||
bin: "myfilter",
|
||||
kind: "missingProfile",
|
||||
isInterpreter: false,
|
||||
},
|
||||
],
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
@@ -58,28 +64,93 @@ describe("doctor exec safe bin helpers", () => {
|
||||
|
||||
expect(warnings).toEqual([
|
||||
"- tools.exec.safeBins includes interpreter/runtime 'node' without profile.",
|
||||
"- agents.list.runner.tools.exec.safeBins includes 'jq': jq supports broad jq programs and builtins (for example `env`), so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
|
||||
"- tools.exec.safeBins entry 'myfilter' is missing safeBinProfiles.myfilter.",
|
||||
"- agents.list.runner.tools.exec.safeBins includes 'jq': jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
|
||||
'- Run "openclaw doctor --fix" to scaffold missing custom safeBinProfiles entries.',
|
||||
]);
|
||||
});
|
||||
|
||||
it("scaffolds custom safeBin profiles but warns on interpreters", () => {
|
||||
it("omits doctor fix hint when no custom safeBin profiles can be scaffolded", () => {
|
||||
const warnings = collectExecSafeBinCoverageWarnings({
|
||||
hits: [
|
||||
{
|
||||
scopePath: "tools.exec",
|
||||
bin: "jq",
|
||||
kind: "riskySemantics",
|
||||
warning:
|
||||
"jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
|
||||
},
|
||||
],
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([
|
||||
"- tools.exec.safeBins includes 'jq': jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("scaffolds custom safeBin profiles but warns on interpreters and risky bins", () => {
|
||||
const result = maybeRepairExecSafeBinProfiles({
|
||||
tools: {
|
||||
exec: {
|
||||
safeBins: ["node", "jq"],
|
||||
safeBins: ["node", "jq", "myfilter"],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig);
|
||||
|
||||
expect(result.changes).toEqual([
|
||||
"- tools.exec.safeBinProfiles.jq: added scaffold profile {} (review and tighten flags/positionals).",
|
||||
"- tools.exec.safeBinProfiles.myfilter: added scaffold profile {} (review and tighten flags/positionals).",
|
||||
]);
|
||||
expect(result.warnings).toEqual([
|
||||
"- tools.exec.safeBins includes 'jq': jq supports broad jq programs and builtins (for example `env`), so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
|
||||
"- tools.exec.safeBins includes 'jq': jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
|
||||
"- tools.exec.safeBins includes interpreter/runtime 'node' without profile; remove it from safeBins or use explicit allowlist entries.",
|
||||
]);
|
||||
expect(result.config.tools?.exec?.safeBinProfiles).toEqual({ jq: {} });
|
||||
expect(result.config.tools?.exec?.safeBinProfiles).toEqual({ myfilter: {} });
|
||||
});
|
||||
|
||||
it("does not scaffold normalized risky safeBins from path-like entries", () => {
|
||||
const hits = scanExecSafeBinCoverage({
|
||||
tools: {
|
||||
exec: {
|
||||
safeBins: ["/usr/local/bin/jq", "sed.exe", "myfilter"],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig);
|
||||
|
||||
expect(hits).toEqual([
|
||||
{ scopePath: "tools.exec", bin: "myfilter", kind: "missingProfile", isInterpreter: false },
|
||||
{
|
||||
scopePath: "tools.exec",
|
||||
bin: "jq",
|
||||
kind: "riskySemantics",
|
||||
warning:
|
||||
"jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
|
||||
},
|
||||
{
|
||||
scopePath: "tools.exec",
|
||||
bin: "sed",
|
||||
kind: "riskySemantics",
|
||||
warning:
|
||||
"sed scripts can execute commands and write files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
|
||||
},
|
||||
]);
|
||||
|
||||
const result = maybeRepairExecSafeBinProfiles({
|
||||
tools: {
|
||||
exec: {
|
||||
safeBins: ["/usr/local/bin/jq", "sed.exe", "myfilter"],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig);
|
||||
|
||||
expect(result.changes).toEqual([
|
||||
"- tools.exec.safeBinProfiles.myfilter: added scaffold profile {} (review and tighten flags/positionals).",
|
||||
]);
|
||||
expect(result.warnings).toEqual([
|
||||
"- tools.exec.safeBins includes 'jq': jq can read environment data and load jq code from modules or startup 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.",
|
||||
]);
|
||||
expect(result.config.tools?.exec?.safeBinProfiles).toEqual({ myfilter: {} });
|
||||
});
|
||||
|
||||
it("warns on awk-family safeBins instead of scaffolding them", () => {
|
||||
@@ -95,10 +166,8 @@ describe("doctor exec safe bin helpers", () => {
|
||||
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).toStrictEqual({});
|
||||
expect(result.config.tools?.exec?.safeBinProfiles).toBeUndefined();
|
||||
});
|
||||
|
||||
it("warns on busybox/toybox safeBins instead of scaffolding them", () => {
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
listInterpreterLikeSafeBins,
|
||||
resolveMergedSafeBinProfileFixtures,
|
||||
} from "../../../infra/exec-safe-bin-runtime-policy.js";
|
||||
import { listRiskyConfiguredSafeBins } from "../../../infra/exec-safe-bin-semantics.js";
|
||||
import {
|
||||
listRiskyConfiguredSafeBins,
|
||||
normalizeSafeBinName,
|
||||
} from "../../../infra/exec-safe-bin-semantics.js";
|
||||
import { getTrustedSafeBinDirs, isTrustedSafeBinPath } from "../../../infra/exec-safe-bin-trust.js";
|
||||
import { asObjectRecord } from "./object.js";
|
||||
|
||||
@@ -103,10 +106,15 @@ export function scanExecSafeBinCoverage(cfg: OpenClawConfig): ExecSafeBinCoverag
|
||||
const hits: ExecSafeBinCoverageHit[] = [];
|
||||
for (const scope of collectExecSafeBinScopes(cfg)) {
|
||||
const interpreterBins = new Set(listInterpreterLikeSafeBins(scope.safeBins));
|
||||
const riskyHits = listRiskyConfiguredSafeBins(scope.safeBins);
|
||||
const riskyBins = new Set(riskyHits.map((hit) => hit.bin));
|
||||
for (const bin of scope.safeBins) {
|
||||
if (scope.mergedProfiles[bin]) {
|
||||
continue;
|
||||
}
|
||||
if (riskyBins.has(normalizeSafeBinName(bin))) {
|
||||
continue;
|
||||
}
|
||||
hits.push({
|
||||
scopePath: scope.scopePath,
|
||||
bin,
|
||||
@@ -114,7 +122,7 @@ export function scanExecSafeBinCoverage(cfg: OpenClawConfig): ExecSafeBinCoverag
|
||||
isInterpreter: interpreterBins.has(bin),
|
||||
});
|
||||
}
|
||||
for (const hit of listRiskyConfiguredSafeBins(scope.safeBins)) {
|
||||
for (const hit of riskyHits) {
|
||||
hits.push({
|
||||
scopePath: scope.scopePath,
|
||||
bin: hit.bin,
|
||||
@@ -205,9 +213,11 @@ export function collectExecSafeBinCoverageWarnings(params: {
|
||||
);
|
||||
}
|
||||
}
|
||||
lines.push(
|
||||
`- Run "${params.doctorFixCommand}" to scaffold missing custom safeBinProfiles entries.`,
|
||||
);
|
||||
if (customHits.length > 0) {
|
||||
lines.push(
|
||||
`- Run "${params.doctorFixCommand}" to scaffold missing custom safeBinProfiles entries.`,
|
||||
);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
@@ -245,10 +255,14 @@ export function maybeRepairExecSafeBinProfiles(cfg: OpenClawConfig): {
|
||||
|
||||
for (const scope of collectExecSafeBinScopes(next)) {
|
||||
const interpreterBins = new Set(listInterpreterLikeSafeBins(scope.safeBins));
|
||||
for (const hit of listRiskyConfiguredSafeBins(scope.safeBins)) {
|
||||
const riskyHits = listRiskyConfiguredSafeBins(scope.safeBins);
|
||||
const riskyBins = new Set(riskyHits.map((hit) => hit.bin));
|
||||
for (const hit of riskyHits) {
|
||||
warnings.push(`- ${scope.scopePath}.safeBins includes '${hit.bin}': ${hit.warning}`);
|
||||
}
|
||||
const missingBins = scope.safeBins.filter((bin) => !scope.mergedProfiles[bin]);
|
||||
const missingBins = scope.safeBins.filter(
|
||||
(bin) => !scope.mergedProfiles[bin] && !riskyBins.has(normalizeSafeBinName(bin)),
|
||||
);
|
||||
if (missingBins.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -114,9 +114,9 @@ describe("exec approvals node host allowlist check", () => {
|
||||
|
||||
it("satisfies via safeBins even when not in allowlist", () => {
|
||||
const resolution = {
|
||||
rawExecutable: "jq",
|
||||
resolvedPath: "/usr/bin/jq",
|
||||
executableName: "jq",
|
||||
rawExecutable: "head",
|
||||
resolvedPath: "/usr/bin/head",
|
||||
executableName: "head",
|
||||
};
|
||||
// Not in allowlist
|
||||
const entries: ExecAllowlistEntry[] = [{ pattern: "/usr/bin/python3" }];
|
||||
@@ -125,9 +125,9 @@ describe("exec approvals node host allowlist check", () => {
|
||||
|
||||
// But is a safe bin with non-file args
|
||||
const safe = isSafeBinUsage({
|
||||
argv: ["jq", ".foo"],
|
||||
argv: ["head", "-n", "1"],
|
||||
resolution,
|
||||
safeBins: normalizeSafeBins(["jq"]),
|
||||
safeBins: normalizeSafeBins(["head"]),
|
||||
});
|
||||
// Safe bins are disabled on Windows (PowerShell parsing/expansion differences).
|
||||
if (process.platform === "win32") {
|
||||
|
||||
@@ -177,10 +177,10 @@ describe("exec approvals safe bins", () => {
|
||||
|
||||
const cases: SafeBinCase[] = [
|
||||
{
|
||||
name: "allows safe bins with non-path args",
|
||||
name: "blocks jq safe bins even with non-path args",
|
||||
argv: ["jq", ".foo"],
|
||||
resolvedPath: "/usr/bin/jq",
|
||||
expected: true,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "blocks jq env builtin even when jq is explicitly opted in",
|
||||
@@ -200,6 +200,30 @@ describe("exec approvals safe bins", () => {
|
||||
resolvedPath: "/usr/bin/jq",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "blocks jq include directives even when jq is explicitly opted in",
|
||||
argv: ["jq", 'include "envdump"; envdump'],
|
||||
resolvedPath: "/usr/bin/jq",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "blocks jq import directives even when jq is explicitly opted in",
|
||||
argv: ["jq", 'import "envdump" as envdump; envdump::read'],
|
||||
resolvedPath: "/usr/bin/jq",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "blocks jq field names that match directive keywords",
|
||||
argv: ["jq", ".include + .import"],
|
||||
resolvedPath: "/usr/bin/jq",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "blocks jq object keys that match directive keywords",
|
||||
argv: ["jq", "{include: .foo, import : .bar}"],
|
||||
resolvedPath: "/usr/bin/jq",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "blocks awk scripts even when awk is explicitly profiled",
|
||||
argv: ["awk", 'BEGIN { system("id") }'],
|
||||
@@ -322,13 +346,13 @@ describe("exec approvals safe bins", () => {
|
||||
return;
|
||||
}
|
||||
const ok = isSafeBinUsage({
|
||||
argv: ["jq", ".foo"],
|
||||
argv: ["head", "-n", "1"],
|
||||
resolution: {
|
||||
rawExecutable: "jq",
|
||||
resolvedPath: "/custom/bin/jq",
|
||||
executableName: "jq",
|
||||
rawExecutable: "head",
|
||||
resolvedPath: "/custom/bin/head",
|
||||
executableName: "head",
|
||||
},
|
||||
safeBins: normalizeSafeBins(["jq"]),
|
||||
safeBins: normalizeSafeBins(["head"]),
|
||||
trustedSafeBinDirs: new Set(["/custom/bin"]),
|
||||
});
|
||||
expect(ok).toBe(true);
|
||||
@@ -339,35 +363,35 @@ describe("exec approvals safe bins", () => {
|
||||
return;
|
||||
}
|
||||
const resolution = {
|
||||
rawExecutable: "jq",
|
||||
resolvedPath: "/opt/homebrew/bin/jq",
|
||||
resolvedRealPath: "/opt/homebrew/Cellar/jq/1.7.1/bin/jq",
|
||||
executableName: "jq",
|
||||
rawExecutable: "head",
|
||||
resolvedPath: "/opt/homebrew/bin/head",
|
||||
resolvedRealPath: "/opt/homebrew/Cellar/coreutils/9.5/bin/head",
|
||||
executableName: "head",
|
||||
};
|
||||
expect(
|
||||
isSafeBinUsage({
|
||||
argv: ["jq", ".foo"],
|
||||
argv: ["head", "-n", "1"],
|
||||
resolution,
|
||||
safeBins: normalizeSafeBins(["jq"]),
|
||||
safeBins: normalizeSafeBins(["head"]),
|
||||
trustedSafeBinDirs: new Set(["/opt/homebrew/bin"]),
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isSafeBinUsage({
|
||||
argv: ["jq", ".foo"],
|
||||
argv: ["head", "-n", "1"],
|
||||
resolution,
|
||||
safeBins: normalizeSafeBins(["jq"]),
|
||||
safeBins: normalizeSafeBins(["head"]),
|
||||
trustedSafeBinDirs: getTrustedSafeBinDirs({
|
||||
extraDirs: ["/opt/homebrew/Cellar/jq/1.7.1/bin"],
|
||||
extraDirs: ["/opt/homebrew/Cellar/coreutils/9.5/bin"],
|
||||
refresh: true,
|
||||
}),
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isSafeBinUsage({
|
||||
argv: ["jq", ".foo"],
|
||||
argv: ["head", "-n", "1"],
|
||||
resolution,
|
||||
safeBins: normalizeSafeBins(["jq"]),
|
||||
safeBins: normalizeSafeBins(["head"]),
|
||||
trustedSafeBinDirs: new Set(["/tmp/other-bin"]),
|
||||
}),
|
||||
).toBe(false);
|
||||
@@ -375,13 +399,13 @@ describe("exec approvals safe bins", () => {
|
||||
|
||||
it("supports injected platform for deterministic safe-bin checks", () => {
|
||||
const ok = isSafeBinUsage({
|
||||
argv: ["jq", ".foo"],
|
||||
argv: ["head", "-n", "1"],
|
||||
resolution: {
|
||||
rawExecutable: "jq",
|
||||
resolvedPath: "/usr/bin/jq",
|
||||
executableName: "jq",
|
||||
rawExecutable: "head",
|
||||
resolvedPath: "/usr/bin/head",
|
||||
executableName: "head",
|
||||
},
|
||||
safeBins: normalizeSafeBins(["jq"]),
|
||||
safeBins: normalizeSafeBins(["head"]),
|
||||
platform: "win32",
|
||||
});
|
||||
expect(ok).toBe(false);
|
||||
@@ -392,13 +416,13 @@ describe("exec approvals safe bins", () => {
|
||||
return;
|
||||
}
|
||||
const baseParams = {
|
||||
argv: ["jq", ".foo"],
|
||||
argv: ["head", "-n", "1"],
|
||||
resolution: {
|
||||
rawExecutable: "jq",
|
||||
resolvedPath: "/tmp/custom/jq",
|
||||
executableName: "jq",
|
||||
rawExecutable: "head",
|
||||
resolvedPath: "/tmp/custom/head",
|
||||
executableName: "head",
|
||||
},
|
||||
safeBins: normalizeSafeBins(["jq"]),
|
||||
safeBins: normalizeSafeBins(["head"]),
|
||||
};
|
||||
expect(
|
||||
isSafeBinUsage({
|
||||
@@ -527,13 +551,13 @@ describe("exec approvals safe bins", () => {
|
||||
ok: true as const,
|
||||
segments: [
|
||||
{
|
||||
raw: "jq .foo",
|
||||
argv: ["jq", ".foo"],
|
||||
raw: "head -n 1",
|
||||
argv: ["head", "-n", "1"],
|
||||
resolution: makeMockCommandResolution({
|
||||
execution: makeMockExecutableResolution({
|
||||
rawExecutable: "jq",
|
||||
resolvedPath: "/custom/bin/jq",
|
||||
executableName: "jq",
|
||||
rawExecutable: "head",
|
||||
resolvedPath: "/custom/bin/head",
|
||||
executableName: "head",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
@@ -542,7 +566,7 @@ describe("exec approvals safe bins", () => {
|
||||
const denied = evaluateExecAllowlist({
|
||||
analysis,
|
||||
allowlist: [],
|
||||
safeBins: normalizeSafeBins(["jq"]),
|
||||
safeBins: normalizeSafeBins(["head"]),
|
||||
trustedSafeBinDirs: new Set(["/usr/bin"]),
|
||||
cwd: "/tmp",
|
||||
});
|
||||
@@ -551,7 +575,7 @@ describe("exec approvals safe bins", () => {
|
||||
const allowed = evaluateExecAllowlist({
|
||||
analysis,
|
||||
allowlist: [],
|
||||
safeBins: normalizeSafeBins(["jq"]),
|
||||
safeBins: normalizeSafeBins(["head"]),
|
||||
trustedSafeBinDirs: new Set(["/custom/bin"]),
|
||||
cwd: "/tmp",
|
||||
});
|
||||
|
||||
@@ -67,13 +67,13 @@ describe("exec approvals allowlist evaluation", () => {
|
||||
ok: true,
|
||||
segments: [
|
||||
{
|
||||
raw: "jq .foo",
|
||||
argv: ["jq", ".foo"],
|
||||
raw: "head -n 1",
|
||||
argv: ["head", "-n", "1"],
|
||||
resolution: makeMockCommandResolution({
|
||||
execution: makeMockExecutableResolution({
|
||||
rawExecutable: "jq",
|
||||
resolvedPath: "/usr/bin/jq",
|
||||
executableName: "jq",
|
||||
rawExecutable: "head",
|
||||
resolvedPath: "/usr/bin/head",
|
||||
executableName: "head",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
@@ -82,7 +82,7 @@ describe("exec approvals allowlist evaluation", () => {
|
||||
const result = evaluateExecAllowlist({
|
||||
analysis,
|
||||
allowlist: [],
|
||||
safeBins: normalizeSafeBins(["jq"]),
|
||||
safeBins: normalizeSafeBins(["head"]),
|
||||
cwd: "/tmp",
|
||||
});
|
||||
// Safe bins are disabled on Windows (PowerShell parsing/expansion differences).
|
||||
@@ -238,13 +238,13 @@ describe("exec approvals allowlist evaluation", () => {
|
||||
}),
|
||||
};
|
||||
const safeBinSegment = {
|
||||
raw: "jq .foo",
|
||||
argv: ["jq", ".foo"],
|
||||
raw: "head -n 1",
|
||||
argv: ["head", "-n", "1"],
|
||||
resolution: makeMockCommandResolution({
|
||||
execution: makeMockExecutableResolution({
|
||||
rawExecutable: "jq",
|
||||
resolvedPath: "/usr/bin/jq",
|
||||
executableName: "jq",
|
||||
rawExecutable: "head",
|
||||
resolvedPath: "/usr/bin/head",
|
||||
executableName: "head",
|
||||
}),
|
||||
}),
|
||||
};
|
||||
@@ -256,7 +256,7 @@ describe("exec approvals allowlist evaluation", () => {
|
||||
const result = evaluateExecAllowlist({
|
||||
analysis,
|
||||
allowlist: [{ pattern: "/usr/bin/tool" }],
|
||||
safeBins: normalizeSafeBins(["jq"]),
|
||||
safeBins: normalizeSafeBins(["head"]),
|
||||
cwd: "/tmp",
|
||||
});
|
||||
if (process.platform === "win32") {
|
||||
|
||||
@@ -78,9 +78,9 @@ describe("exec safe bin policy grep", () => {
|
||||
describe("exec safe bin policy jq", () => {
|
||||
const jqProfile = SAFE_BIN_PROFILES.jq;
|
||||
|
||||
it("allows normal jq field filters", () => {
|
||||
expect(validateSafeBinArgv([".foo"], jqProfile, { binName: "jq" })).toBe(true);
|
||||
expect(validateSafeBinArgv([".env"], jqProfile, { binName: "jq" })).toBe(true);
|
||||
it("blocks normal jq field filters in safe-bin mode", () => {
|
||||
expect(validateSafeBinArgv([".foo"], jqProfile, { binName: "jq" })).toBe(false);
|
||||
expect(validateSafeBinArgv([".env"], jqProfile, { binName: "jq" })).toBe(false);
|
||||
});
|
||||
|
||||
it("blocks jq env builtin filters in safe-bin mode", () => {
|
||||
|
||||
@@ -6,6 +6,21 @@ import {
|
||||
} from "./exec-safe-bin-semantics.js";
|
||||
|
||||
describe("exec safe-bin semantics", () => {
|
||||
it("rejects jq programs even when configured via path-like entries", () => {
|
||||
expect(
|
||||
validateSafeBinSemantics({
|
||||
binName: "jq",
|
||||
positional: [".foo"],
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
validateSafeBinSemantics({
|
||||
binName: "/usr/local/bin/jq",
|
||||
positional: ["{include: .foo, import : .bar}"],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects awk and sed variants even when configured via path-like entries", () => {
|
||||
expect(
|
||||
validateSafeBinSemantics({
|
||||
@@ -64,7 +79,7 @@ describe("exec safe-bin semantics", () => {
|
||||
{
|
||||
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.",
|
||||
"jq can read environment data and load jq code from modules or startup files, so prefer explicit allowlist entries or approval-gated runs instead of safeBins.",
|
||||
},
|
||||
{
|
||||
bin: "mawk",
|
||||
|
||||
@@ -11,22 +11,17 @@ type SafeBinSemanticRule = {
|
||||
configWarning?: string;
|
||||
};
|
||||
|
||||
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.",
|
||||
jq: "jq can read environment data and load jq code from modules or startup files, 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: {
|
||||
validate: ({ positional }) =>
|
||||
!positional.some(
|
||||
(token) => JQ_ENV_FILTER_PATTERN.test(token) || JQ_ENV_VARIABLE_PATTERN.test(token),
|
||||
),
|
||||
validate: ALWAYS_DENY_SAFE_BIN_SEMANTICS,
|
||||
configWarning: UNSAFE_SAFE_BIN_WARNINGS.jq,
|
||||
},
|
||||
awk: {
|
||||
|
||||
Reference in New Issue
Block a user