fix(security): flag open-group runtime/fs exposure in audit

This commit is contained in:
Peter Steinberger
2026-02-22 08:22:42 +01:00
parent 17c9d550e9
commit 049b8b14bc
5 changed files with 145 additions and 24 deletions

View File

@@ -1041,5 +1041,67 @@ export function collectExposureMatrixFindings(cfg: OpenClawConfig): SecurityAudi
});
}
const contexts: Array<{
label: string;
agentId?: string;
tools?: AgentToolsConfig;
}> = [{ label: "agents.defaults" }];
for (const agent of cfg.agents?.list ?? []) {
if (!agent || typeof agent !== "object" || typeof agent.id !== "string") {
continue;
}
contexts.push({
label: `agents.list.${agent.id}`,
agentId: agent.id,
tools: agent.tools,
});
}
const riskyContexts: string[] = [];
let hasRuntimeRisk = false;
for (const context of contexts) {
const sandboxMode = resolveSandboxConfigForAgent(cfg, context.agentId).mode;
const policies = resolveToolPolicies({
cfg,
agentTools: context.tools,
sandboxMode,
agentId: context.agentId ?? null,
});
const runtimeTools = ["exec", "process"].filter((tool) =>
isToolAllowedByPolicies(tool, policies),
);
const fsTools = ["read", "write", "edit", "apply_patch"].filter((tool) =>
isToolAllowedByPolicies(tool, policies),
);
const fsWorkspaceOnly = context.tools?.fs?.workspaceOnly ?? cfg.tools?.fs?.workspaceOnly;
const runtimeUnguarded = runtimeTools.length > 0 && sandboxMode !== "all";
const fsUnguarded = fsTools.length > 0 && sandboxMode !== "all" && fsWorkspaceOnly !== true;
if (!runtimeUnguarded && !fsUnguarded) {
continue;
}
if (runtimeUnguarded) {
hasRuntimeRisk = true;
}
riskyContexts.push(
`${context.label} (sandbox=${sandboxMode}; runtime=[${runtimeTools.join(", ") || "off"}]; fs=[${fsTools.join(", ") || "off"}]; fs.workspaceOnly=${
fsWorkspaceOnly === true ? "true" : "false"
})`,
);
}
if (riskyContexts.length > 0) {
findings.push({
checkId: "security.exposure.open_groups_with_runtime_or_fs",
severity: hasRuntimeRisk ? "critical" : "warn",
title: "Open groupPolicy with runtime/filesystem tools exposed",
detail:
`Found groupPolicy="open" at:\n${openGroups.map((p) => `- ${p}`).join("\n")}\n` +
`Risky tool exposure contexts:\n${riskyContexts.map((line) => `- ${line}`).join("\n")}\n` +
"Prompt injection in open groups can trigger command/file actions in these contexts.",
remediation:
'For open groups, prefer tools.profile="messaging" (or deny group:runtime/group:fs), set tools.fs.workspaceOnly=true, and use agents.defaults.sandbox.mode="all" for exposed agents.',
});
}
return findings;
}

View File

@@ -2150,6 +2150,63 @@ description: test skill
);
});
it("flags open groupPolicy when runtime/filesystem tools are exposed without guards", async () => {
const cfg: OpenClawConfig = {
channels: { whatsapp: { groupPolicy: "open" } },
tools: { elevated: { enabled: false } },
};
const res = await audit(cfg);
expect(res.findings).toEqual(
expect.arrayContaining([
expect.objectContaining({
checkId: "security.exposure.open_groups_with_runtime_or_fs",
severity: "critical",
}),
]),
);
});
it("does not flag runtime/filesystem exposure for open groups when sandbox mode is all", async () => {
const cfg: OpenClawConfig = {
channels: { whatsapp: { groupPolicy: "open" } },
tools: {
elevated: { enabled: false },
profile: "coding",
},
agents: {
defaults: {
sandbox: { mode: "all" },
},
},
};
const res = await audit(cfg);
expect(
res.findings.some((f) => f.checkId === "security.exposure.open_groups_with_runtime_or_fs"),
).toBe(false);
});
it("does not flag runtime/filesystem exposure for open groups when runtime is denied and fs is workspace-only", async () => {
const cfg: OpenClawConfig = {
channels: { whatsapp: { groupPolicy: "open" } },
tools: {
elevated: { enabled: false },
profile: "coding",
deny: ["group:runtime"],
fs: { workspaceOnly: true },
},
};
const res = await audit(cfg);
expect(
res.findings.some((f) => f.checkId === "security.exposure.open_groups_with_runtime_or_fs"),
).toBe(false);
});
describe("maybeProbeGateway auth selection", () => {
let envSnapshot: ReturnType<typeof captureEnv>;