mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-31 11:51:22 +00:00
fix(security): harden exec approval boundaries
This commit is contained in:
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { saveExecApprovals } from "../infra/exec-approvals.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import {
|
||||
collectInstalledSkillsCodeSafetyFindings,
|
||||
@@ -167,13 +168,17 @@ function successfulProbeResult(url: string) {
|
||||
|
||||
async function audit(
|
||||
cfg: OpenClawConfig,
|
||||
extra?: Omit<SecurityAuditOptions, "config">,
|
||||
extra?: Omit<SecurityAuditOptions, "config"> & { preserveExecApprovals?: boolean },
|
||||
): Promise<SecurityAuditReport> {
|
||||
if (!extra?.preserveExecApprovals) {
|
||||
saveExecApprovals({ version: 1, agents: {} });
|
||||
}
|
||||
const { preserveExecApprovals: _preserveExecApprovals, ...options } = extra ?? {};
|
||||
return runSecurityAudit({
|
||||
config: cfg,
|
||||
includeFilesystem: false,
|
||||
includeChannelSecurity: false,
|
||||
...extra,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -242,6 +247,7 @@ describe("security audit", () => {
|
||||
let sharedCodeSafetyWorkspaceDir = "";
|
||||
let sharedExtensionsStateDir = "";
|
||||
let sharedInstallMetadataStateDir = "";
|
||||
let previousOpenClawHome: string | undefined;
|
||||
|
||||
const makeTmpDir = async (label: string) => {
|
||||
const dir = path.join(fixtureRoot, `case-${caseId++}-${label}`);
|
||||
@@ -323,6 +329,9 @@ description: test skill
|
||||
|
||||
beforeAll(async () => {
|
||||
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-security-audit-"));
|
||||
previousOpenClawHome = process.env.OPENCLAW_HOME;
|
||||
process.env.OPENCLAW_HOME = path.join(fixtureRoot, "home");
|
||||
await fs.mkdir(process.env.OPENCLAW_HOME, { recursive: true, mode: 0o700 });
|
||||
channelSecurityRoot = path.join(fixtureRoot, "channel-security");
|
||||
await fs.mkdir(channelSecurityRoot, { recursive: true, mode: 0o700 });
|
||||
sharedChannelSecurityStateDir = path.join(channelSecurityRoot, "state-shared");
|
||||
@@ -343,6 +352,11 @@ description: test skill
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (previousOpenClawHome === undefined) {
|
||||
delete process.env.OPENCLAW_HOME;
|
||||
} else {
|
||||
process.env.OPENCLAW_HOME = previousOpenClawHome;
|
||||
}
|
||||
if (!fixtureRoot) {
|
||||
return;
|
||||
}
|
||||
@@ -732,6 +746,105 @@ description: test skill
|
||||
);
|
||||
});
|
||||
|
||||
it("warns when exec approvals enable autoAllowSkills", async () => {
|
||||
saveExecApprovals({
|
||||
version: 1,
|
||||
defaults: {
|
||||
autoAllowSkills: true,
|
||||
},
|
||||
agents: {},
|
||||
});
|
||||
|
||||
const res = await audit({}, { preserveExecApprovals: true });
|
||||
expectFinding(res, "tools.exec.auto_allow_skills_enabled", "warn");
|
||||
saveExecApprovals({ version: 1, agents: {} });
|
||||
});
|
||||
|
||||
it("warns when interpreter allowlists are present without strictInlineEval", async () => {
|
||||
saveExecApprovals({
|
||||
version: 1,
|
||||
agents: {
|
||||
main: {
|
||||
allowlist: [{ pattern: "/usr/bin/python3" }],
|
||||
},
|
||||
ops: {
|
||||
allowlist: [{ pattern: "/usr/local/bin/node" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const res = await audit(
|
||||
{
|
||||
agents: {
|
||||
list: [{ id: "ops" }],
|
||||
},
|
||||
},
|
||||
{ preserveExecApprovals: true },
|
||||
);
|
||||
expectFinding(res, "tools.exec.allowlist_interpreter_without_strict_inline_eval", "warn");
|
||||
saveExecApprovals({ version: 1, agents: {} });
|
||||
});
|
||||
|
||||
it("suppresses interpreter allowlist warnings when strictInlineEval is enabled", async () => {
|
||||
saveExecApprovals({
|
||||
version: 1,
|
||||
agents: {
|
||||
main: {
|
||||
allowlist: [{ pattern: "/usr/bin/python3" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const res = await audit(
|
||||
{
|
||||
tools: {
|
||||
exec: {
|
||||
strictInlineEval: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ preserveExecApprovals: true },
|
||||
);
|
||||
expectNoFinding(res, "tools.exec.allowlist_interpreter_without_strict_inline_eval");
|
||||
saveExecApprovals({ version: 1, agents: {} });
|
||||
});
|
||||
|
||||
it("flags open channel access combined with exec-enabled scopes", async () => {
|
||||
const res = await audit({
|
||||
channels: {
|
||||
discord: {
|
||||
groupPolicy: "open",
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
exec: {
|
||||
security: "allowlist",
|
||||
host: "gateway",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expectFinding(res, "security.exposure.open_channels_with_exec", "warn");
|
||||
});
|
||||
|
||||
it("escalates open channel exec exposure when full exec is configured", async () => {
|
||||
const res = await audit({
|
||||
channels: {
|
||||
slack: {
|
||||
dmPolicy: "open",
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
exec: {
|
||||
security: "full",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expectFinding(res, "tools.exec.security_full_configured", "critical");
|
||||
expectFinding(res, "security.exposure.open_channels_with_exec", "critical");
|
||||
});
|
||||
|
||||
it("evaluates loopback control UI and logging exposure findings", async () => {
|
||||
const cases: Array<{
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user