refactor(exec): centralize safe-bin policy checks

This commit is contained in:
Peter Steinberger
2026-02-22 13:18:17 +01:00
parent bcad4f67a2
commit 0d0f4c6992
15 changed files with 806 additions and 68 deletions

View File

@@ -296,6 +296,70 @@ describe("security audit", () => {
expect(hasFinding(res, "tools.exec.host_sandbox_no_sandbox_agents", "warn")).toBe(true);
});
it("warns for interpreter safeBins entries without explicit profiles", async () => {
const cfg: OpenClawConfig = {
tools: {
exec: {
safeBins: ["python3"],
},
},
agents: {
list: [
{
id: "ops",
tools: {
exec: {
safeBins: ["node"],
},
},
},
],
},
};
const res = await audit(cfg);
expect(hasFinding(res, "tools.exec.safe_bins_interpreter_unprofiled", "warn")).toBe(true);
});
it("does not warn for interpreter safeBins when explicit profiles are present", async () => {
const cfg: OpenClawConfig = {
tools: {
exec: {
safeBins: ["python3"],
safeBinProfiles: {
python3: {
maxPositional: 0,
},
},
},
},
agents: {
list: [
{
id: "ops",
tools: {
exec: {
safeBins: ["node"],
safeBinProfiles: {
node: {
maxPositional: 0,
},
},
},
},
},
],
},
};
const res = await audit(cfg);
expect(
res.findings.some((f) => f.checkId === "tools.exec.safe_bins_interpreter_unprofiled"),
).toBe(false);
});
it("warns when loopback control UI lacks trusted proxies", async () => {
const cfg: OpenClawConfig = {
gateway: {

View File

@@ -11,6 +11,10 @@ import { resolveGatewayAuth } from "../gateway/auth.js";
import { buildGatewayConnectionDetails } from "../gateway/call.js";
import { resolveGatewayProbeAuth } from "../gateway/probe-auth.js";
import { probeGateway } from "../gateway/probe.js";
import {
listInterpreterLikeSafeBins,
resolveMergedSafeBinProfileFixtures,
} from "../infra/exec-safe-bin-runtime-policy.js";
import { collectChannelSecurityFindings } from "./audit-channel.js";
import {
collectAttackSurfaceSummaryFindings,
@@ -695,6 +699,65 @@ function collectExecRuntimeFindings(cfg: OpenClawConfig): SecurityAuditFinding[]
});
}
const normalizeConfiguredSafeBins = (entries: unknown): string[] => {
if (!Array.isArray(entries)) {
return [];
}
return Array.from(
new Set(
entries
.map((entry) => (typeof entry === "string" ? entry.trim().toLowerCase() : ""))
.filter((entry) => entry.length > 0),
),
).toSorted();
};
const interpreterHits: string[] = [];
const globalExec = cfg.tools?.exec;
const globalSafeBins = normalizeConfiguredSafeBins(globalExec?.safeBins);
if (globalSafeBins.length > 0) {
const merged = resolveMergedSafeBinProfileFixtures({ global: globalExec }) ?? {};
const interpreters = listInterpreterLikeSafeBins(globalSafeBins).filter((bin) => !merged[bin]);
if (interpreters.length > 0) {
interpreterHits.push(`- tools.exec.safeBins: ${interpreters.join(", ")}`);
}
}
for (const entry of agents) {
if (!entry || typeof entry !== "object" || typeof entry.id !== "string") {
continue;
}
const agentExec = entry.tools?.exec;
const agentSafeBins = normalizeConfiguredSafeBins(agentExec?.safeBins);
if (agentSafeBins.length === 0) {
continue;
}
const merged =
resolveMergedSafeBinProfileFixtures({
global: globalExec,
local: agentExec,
}) ?? {};
const interpreters = listInterpreterLikeSafeBins(agentSafeBins).filter((bin) => !merged[bin]);
if (interpreters.length === 0) {
continue;
}
interpreterHits.push(
`- agents.list.${entry.id}.tools.exec.safeBins: ${interpreters.join(", ")}`,
);
}
if (interpreterHits.length > 0) {
findings.push({
checkId: "tools.exec.safe_bins_interpreter_unprofiled",
severity: "warn",
title: "safeBins includes interpreter/runtime binaries without explicit profiles",
detail:
`Detected interpreter-like safeBins entries missing explicit profiles:\n${interpreterHits.join("\n")}\n` +
"These entries can turn safeBins into a broad execution surface when used with permissive argv profiles.",
remediation:
"Remove interpreter/runtime bins from safeBins (prefer allowlist entries) or define hardened tools.exec.safeBinProfiles.<bin> rules.",
});
}
return findings;
}