fix(security): harden sandbox docker config validation

This commit is contained in:
Peter Steinberger
2026-02-16 03:03:55 +01:00
parent d4bdcda324
commit 887b209db4
11 changed files with 691 additions and 6 deletions

View File

@@ -11,6 +11,7 @@ import {
resolveSandboxConfigForAgent,
resolveSandboxToolPolicyForAgent,
} from "../agents/sandbox.js";
import { getBlockedBindReasonStringOnly } from "../agents/sandbox/validate-sandbox-security.js";
import { resolveToolProfilePolicy } from "../agents/tool-policy.js";
import { resolveBrowserConfig } from "../browser/config.js";
import { formatCliCommand } from "../cli/command-format.js";
@@ -584,6 +585,104 @@ export function collectSandboxDockerNoopFindings(cfg: OpenClawConfig): SecurityA
return findings;
}
export function collectSandboxDangerousConfigFindings(cfg: OpenClawConfig): SecurityAuditFinding[] {
const findings: SecurityAuditFinding[] = [];
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
const configs: Array<{ source: string; docker: Record<string, unknown> }> = [];
const defaultDocker = cfg.agents?.defaults?.sandbox?.docker;
if (defaultDocker && typeof defaultDocker === "object") {
configs.push({
source: "agents.defaults.sandbox.docker",
docker: defaultDocker as Record<string, unknown>,
});
}
for (const entry of agents) {
if (!entry || typeof entry !== "object" || typeof entry.id !== "string") {
continue;
}
const agentDocker = entry.sandbox?.docker;
if (agentDocker && typeof agentDocker === "object") {
configs.push({
source: `agents.list.${entry.id}.sandbox.docker`,
docker: agentDocker as Record<string, unknown>,
});
}
}
for (const { source, docker } of configs) {
const binds = Array.isArray(docker.binds) ? docker.binds : [];
for (const bind of binds) {
if (typeof bind !== "string") {
continue;
}
const blocked = getBlockedBindReasonStringOnly(bind);
if (!blocked) {
continue;
}
if (blocked.kind === "non_absolute") {
findings.push({
checkId: "sandbox.bind_mount_non_absolute",
severity: "warn",
title: "Sandbox bind mount uses a non-absolute source path",
detail:
`${source}.binds contains "${bind}" which uses source path "${blocked.sourcePath}". ` +
"Non-absolute bind sources are hard to validate safely and may resolve unexpectedly.",
remediation: `Rewrite "${bind}" to use an absolute host path (for example: /home/user/project:/project:ro).`,
});
continue;
}
const verb = blocked.kind === "covers" ? "covers" : "targets";
findings.push({
checkId: "sandbox.dangerous_bind_mount",
severity: "critical",
title: "Dangerous bind mount in sandbox config",
detail:
`${source}.binds contains "${bind}" which ${verb} blocked path "${blocked.blockedPath}". ` +
"This can expose host system directories or the Docker socket to sandbox containers.",
remediation: `Remove "${bind}" from ${source}.binds. Use project-specific paths instead.`,
});
}
const network = typeof docker.network === "string" ? docker.network : undefined;
if (network && network.trim().toLowerCase() === "host") {
findings.push({
checkId: "sandbox.dangerous_network_mode",
severity: "critical",
title: "Network host mode in sandbox config",
detail: `${source}.network is "host" which bypasses container network isolation entirely.`,
remediation: `Set ${source}.network to "bridge" or "none".`,
});
}
const seccompProfile =
typeof docker.seccompProfile === "string" ? docker.seccompProfile : undefined;
if (seccompProfile && seccompProfile.trim().toLowerCase() === "unconfined") {
findings.push({
checkId: "sandbox.dangerous_seccomp_profile",
severity: "critical",
title: "Seccomp unconfined in sandbox config",
detail: `${source}.seccompProfile is "unconfined" which disables syscall filtering.`,
remediation: `Remove ${source}.seccompProfile or use a custom seccomp profile file.`,
});
}
const apparmorProfile =
typeof docker.apparmorProfile === "string" ? docker.apparmorProfile : undefined;
if (apparmorProfile && apparmorProfile.trim().toLowerCase() === "unconfined") {
findings.push({
checkId: "sandbox.dangerous_apparmor_profile",
severity: "critical",
title: "AppArmor unconfined in sandbox config",
detail: `${source}.apparmorProfile is "unconfined" which disables AppArmor enforcement.`,
remediation: `Remove ${source}.apparmorProfile or use a named AppArmor profile.`,
});
}
}
return findings;
}
export function collectNodeDenyCommandPatternFindings(cfg: OpenClawConfig): SecurityAuditFinding[] {
const findings: SecurityAuditFinding[] = [];
const denyListRaw = cfg.gateway?.nodes?.denyCommands;