From 14b6eea6e30e805e2147db900a301f074dade96e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 24 Feb 2026 23:19:48 +0000 Subject: [PATCH] feat(sandbox): block container namespace joins by default --- CHANGELOG.md | 4 ++ docs/cli/security.md | 1 + docs/gateway/configuration-reference.md | 4 +- docs/gateway/sandboxing.md | 7 ++ docs/gateway/security/index.md | 3 + docs/install/docker.md | 8 ++- src/agents/sandbox-create-args.test.ts | 20 ++++++ src/agents/sandbox/browser.ts | 20 +++--- src/agents/sandbox/config.ts | 9 +++ src/agents/sandbox/docker.ts | 4 ++ .../sandbox/validate-sandbox-security.test.ts | 24 +++++++ .../sandbox/validate-sandbox-security.ts | 29 ++++++++- src/config/config.sandbox-docker.test.ts | 64 +++++++++++++++++++ src/config/types.sandbox.ts | 5 ++ src/config/zod-schema.agent-runtime.ts | 28 +++++++- src/security/audit-extra.sync.ts | 16 +++-- src/security/audit.test.ts | 25 ++++++++ 17 files changed, 253 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcbd4cb1e56..62beb85b694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ Docs: https://docs.openclaw.ai - Auto-reply/Abort shortcuts: expand standalone stop phrases (`stop openclaw`, `stop action`, `stop run`, `stop agent`, `please stop`, and related variants), accept trailing punctuation (for example `STOP OPENCLAW!!!`), and add multilingual stop keywords (including ES/FR/ZH/HI/AR/JP/DE/PT/RU forms) so emergency stop messages are caught more reliably. (#25103) Thanks @steipete and @vincentkoc. - Security/Audit: add `security.trust_model.multi_user_heuristic` to flag likely shared-user ingress and clarify the personal-assistant trust model, with hardening guidance for intentional multi-user setups (`sandbox.mode="all"`, workspace-scoped FS, reduced tool surface, no personal/private identities on shared runtimes). +### Breaking + +- **BREAKING:** Security/Sandbox: block Docker `network: "container:"` namespace-join mode by default for sandbox and sandbox-browser containers. To keep that behavior intentionally, set `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin: true` (break-glass). Thanks @tdjackey for reporting. + ### Fixes - Routing/Session isolation: harden followup routing so explicit cross-channel origin replies never fall back to the active dispatcher on route failure, preserve queued overflow summary routing metadata (`channel`/`to`/`thread`) across followup drain, and prefer originating channel context over internal provider tags for embedded followup runs. This prevents webchat/control-ui context from hijacking Discord-targeted replies in shared sessions. (#25864) Thanks @Gamedesigner. diff --git a/docs/cli/security.md b/docs/cli/security.md index b962ebef675..fe8af41ec25 100644 --- a/docs/cli/security.md +++ b/docs/cli/security.md @@ -32,6 +32,7 @@ For webhook ingress, it warns when `hooks.defaultSessionKey` is unset, when requ It also warns when sandbox Docker settings are configured while sandbox mode is off, when `gateway.nodes.denyCommands` uses ineffective pattern-like/unknown entries, when `gateway.nodes.allowCommands` explicitly enables dangerous node commands, when global `tools.profile="minimal"` is overridden by agent tool profiles, when open groups expose runtime/filesystem tools without sandbox/workspace guards, and when installed extension plugin tools may be reachable under permissive tool policy. It also flags `gateway.allowRealIpFallback=true` (header-spoofing risk if proxies are misconfigured) and `discovery.mdns.mode="full"` (metadata leakage via mDNS TXT records). It also warns when sandbox browser uses Docker `bridge` network without `sandbox.browser.cdpSourceRange`. +It also flags dangerous sandbox Docker network modes (including `host` and `container:*` namespace joins). It also warns when existing sandbox browser Docker containers have missing/stale hash labels (for example pre-migration containers missing `openclaw.browserConfigEpoch`) and recommends `openclaw sandbox recreate --browser --all`. It also warns when npm-based plugin/hook install records are unpinned, missing integrity metadata, or drift from currently installed package versions. It warns when channel allowlists rely on mutable names/emails/tags instead of stable IDs (Discord, Slack, Google Chat, MS Teams, Mattermost, IRC scopes where applicable). diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 0b89a272d90..825acbaadf5 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -1017,7 +1017,9 @@ Optional **Docker sandboxing** for the embedded agent. See [Sandboxing](/gateway **`setupCommand`** runs once after container creation (via `sh -lc`). Needs network egress, writable root, root user. -**Containers default to `network: "none"`** — set to `"bridge"` if the agent needs outbound access. +**Containers default to `network: "none"`** — set to `"bridge"` (or a custom bridge network) if the agent needs outbound access. +`"host"` is blocked. `"container:"` is blocked by default unless you explicitly set +`sandbox.docker.dangerouslyAllowContainerNamespaceJoin: true` (break-glass). **Inbound attachments** are staged into `media/inbound/*` in the active workspace. diff --git a/docs/gateway/sandboxing.md b/docs/gateway/sandboxing.md index 6d51f573990..8be57bd1064 100644 --- a/docs/gateway/sandboxing.md +++ b/docs/gateway/sandboxing.md @@ -138,6 +138,12 @@ scripts/sandbox-browser-setup.sh By default, sandbox containers run with **no network**. Override with `agents.defaults.sandbox.docker.network`. +Security defaults: + +- `network: "host"` is blocked. +- `network: "container:"` is blocked by default (namespace join bypass risk). +- Break-glass override: `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin: true`. + Docker installs and the containerized gateway live here: [Docker](/install/docker) @@ -154,6 +160,7 @@ Paths: Common pitfalls: - Default `docker.network` is `"none"` (no egress), so package installs will fail. +- `docker.network: "container:"` requires `dangerouslyAllowContainerNamespaceJoin: true` and is break-glass only. - `readOnlyRoot: true` prevents writes; set `readOnlyRoot: false` or bake a custom image. - `user` must be root for package installs (omit `user` or set `user: "0:0"`). - Sandbox exec does **not** inherit host `process.env`. Use diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index c0d642b0e55..c9c3f4051e4 100644 --- a/docs/gateway/security/index.md +++ b/docs/gateway/security/index.md @@ -244,6 +244,7 @@ High-signal `checkId` values you will most likely see in real deployments (not e | `hooks.request_session_key_prefixes_missing` | warn/critical | No bound on external session key shapes | `hooks.allowedSessionKeyPrefixes` | no | | `logging.redact_off` | warn | Sensitive values leak to logs/status | `logging.redactSensitive` | yes | | `sandbox.docker_config_mode_off` | warn | Sandbox Docker config present but inactive | `agents.*.sandbox.mode` | no | +| `sandbox.dangerous_network_mode` | critical | Sandbox Docker network uses `host` or `container:*` namespace-join mode | `agents.*.sandbox.docker.network` | no | | `tools.exec.host_sandbox_no_sandbox_defaults` | warn | `exec host=sandbox` resolves to host exec when sandbox is off | `tools.exec.host`, `agents.defaults.sandbox.mode` | no | | `tools.exec.host_sandbox_no_sandbox_agents` | warn | Per-agent `exec host=sandbox` resolves to host exec when sandbox is off | `agents.list[].tools.exec.host`, `agents.list[].sandbox.mode` | no | | `tools.exec.safe_bins_interpreter_unprofiled` | warn | Interpreter/runtime bins in `safeBins` without explicit profiles broaden exec risk | `tools.exec.safeBins`, `tools.exec.safeBinProfiles`, `agents.list[].tools.exec.*` | no | @@ -299,8 +300,10 @@ schema: - `channels.mattermost.accounts..dangerouslyAllowNameMatching` (extension channel) - `agents.defaults.sandbox.docker.dangerouslyAllowReservedContainerTargets` - `agents.defaults.sandbox.docker.dangerouslyAllowExternalBindSources` +- `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin` - `agents.list[].sandbox.docker.dangerouslyAllowReservedContainerTargets` - `agents.list[].sandbox.docker.dangerouslyAllowExternalBindSources` +- `agents.list[].sandbox.docker.dangerouslyAllowContainerNamespaceJoin` ## Reverse Proxy Configuration diff --git a/docs/install/docker.md b/docs/install/docker.md index 8826192c1c1..decd1d779ee 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -368,6 +368,8 @@ precedence, and troubleshooting. - `"rw"` mounts the agent workspace read/write at `/workspace` - Auto-prune: idle > 24h OR age > 7d - Network: `none` by default (explicitly opt-in if you need egress) + - `host` is blocked. + - `container:` is blocked by default (namespace-join risk). - Default allow: `exec`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` - Default deny: `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway` @@ -376,6 +378,9 @@ precedence, and troubleshooting. If you plan to install packages in `setupCommand`, note: - Default `docker.network` is `"none"` (no egress). +- `docker.network: "host"` is blocked. +- `docker.network: "container:"` is blocked by default. +- Break-glass override: `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin: true`. - `readOnlyRoot: true` blocks package installs. - `user` must be root for `apt-get` (omit `user` or set `user: "0:0"`). OpenClaw auto-recreates containers when `setupCommand` (or docker config) changes @@ -445,7 +450,8 @@ If you plan to install packages in `setupCommand`, note: Hardening knobs live under `agents.defaults.sandbox.docker`: `network`, `user`, `pidsLimit`, `memory`, `memorySwap`, `cpus`, `ulimits`, -`seccompProfile`, `apparmorProfile`, `dns`, `extraHosts`. +`seccompProfile`, `apparmorProfile`, `dns`, `extraHosts`, +`dangerouslyAllowContainerNamespaceJoin` (break-glass only). Multi-agent: override `agents.defaults.sandbox.{docker,browser,prune}.*` per agent via `agents.list[].sandbox.{docker,browser,prune}.*` (ignored when `agents.defaults.sandbox.scope` / `agents.list[].sandbox.scope` is `"shared"`). diff --git a/src/agents/sandbox-create-args.test.ts b/src/agents/sandbox-create-args.test.ts index 2347b88fc3e..9bc00547143 100644 --- a/src/agents/sandbox-create-args.test.ts +++ b/src/agents/sandbox-create-args.test.ts @@ -181,6 +181,12 @@ describe("buildSandboxCreateArgs", () => { cfg: createSandboxConfig({ network: "host" }), expected: /network mode "host" is blocked/, }, + { + name: "network container namespace join", + containerName: "openclaw-sbx-container-network", + cfg: createSandboxConfig({ network: "container:peer" }), + expected: /network mode "container:peer" is blocked by default/, + }, { name: "seccomp unconfined", containerName: "openclaw-sbx-seccomp", @@ -271,4 +277,18 @@ describe("buildSandboxCreateArgs", () => { }); expect(args).toEqual(expect.arrayContaining(["-v", "/tmp/override:/workspace:rw"])); }); + + it("allows container namespace join with explicit dangerous override", () => { + const cfg = createSandboxConfig({ + network: "container:peer", + dangerouslyAllowContainerNamespaceJoin: true, + }); + const args = buildSandboxCreateArgs({ + name: "openclaw-sbx-container-network-override", + cfg, + scopeKey: "main", + createdAtMs: 1700000000000, + }); + expect(args).toEqual(expect.arrayContaining(["--network", "container:peer"])); + }); }); diff --git a/src/agents/sandbox/browser.ts b/src/agents/sandbox/browser.ts index f96261bfab7..c4459b19bdd 100644 --- a/src/agents/sandbox/browser.ts +++ b/src/agents/sandbox/browser.ts @@ -36,6 +36,7 @@ import { readBrowserRegistry, updateBrowserRegistry } from "./registry.js"; import { resolveSandboxAgentId, slugifySessionKey } from "./shared.js"; import { isToolAllowed } from "./tool-policy.js"; import type { SandboxBrowserContext, SandboxConfig } from "./types.js"; +import { validateNetworkMode } from "./validate-sandbox-security.js"; const HOT_BROWSER_WINDOW_MS = 5 * 60 * 1000; const CDP_SOURCE_RANGE_ENV_KEY = "OPENCLAW_BROWSER_CDP_SOURCE_RANGE"; @@ -107,14 +108,15 @@ async function ensureSandboxBrowserImage(image: string) { ); } -async function ensureDockerNetwork(network: string) { +async function ensureDockerNetwork( + network: string, + opts?: { allowContainerNamespaceJoin?: boolean }, +) { + validateNetworkMode(network, { + allowContainerNamespaceJoin: opts?.allowContainerNamespaceJoin === true, + }); const normalized = network.trim().toLowerCase(); - if ( - !normalized || - normalized === "bridge" || - normalized === "none" || - normalized.startsWith("container:") - ) { + if (!normalized || normalized === "bridge" || normalized === "none") { return; } const inspect = await execDocker(["network", "inspect", network], { allowFailure: true }); @@ -216,7 +218,9 @@ export async function ensureSandboxBrowser(params: { if (noVncEnabled) { noVncPassword = generateNoVncPassword(); } - await ensureDockerNetwork(browserDockerCfg.network); + await ensureDockerNetwork(browserDockerCfg.network, { + allowContainerNamespaceJoin: browserDockerCfg.dangerouslyAllowContainerNamespaceJoin === true, + }); await ensureSandboxBrowserImage(browserImage); const args = buildSandboxCreateArgs({ name: containerName, diff --git a/src/agents/sandbox/config.ts b/src/agents/sandbox/config.ts index 0fcb50999e4..135c9a6520b 100644 --- a/src/agents/sandbox/config.ts +++ b/src/agents/sandbox/config.ts @@ -95,6 +95,15 @@ export function resolveSandboxDockerConfig(params: { dns: agentDocker?.dns ?? globalDocker?.dns, extraHosts: agentDocker?.extraHosts ?? globalDocker?.extraHosts, binds: binds.length ? binds : undefined, + dangerouslyAllowReservedContainerTargets: + agentDocker?.dangerouslyAllowReservedContainerTargets ?? + globalDocker?.dangerouslyAllowReservedContainerTargets, + dangerouslyAllowExternalBindSources: + agentDocker?.dangerouslyAllowExternalBindSources ?? + globalDocker?.dangerouslyAllowExternalBindSources, + dangerouslyAllowContainerNamespaceJoin: + agentDocker?.dangerouslyAllowContainerNamespaceJoin ?? + globalDocker?.dangerouslyAllowContainerNamespaceJoin, }; } diff --git a/src/agents/sandbox/docker.ts b/src/agents/sandbox/docker.ts index 270e8b761d4..efaa4b0e22e 100644 --- a/src/agents/sandbox/docker.ts +++ b/src/agents/sandbox/docker.ts @@ -267,6 +267,7 @@ export function buildSandboxCreateArgs(params: { bindSourceRoots?: string[]; allowSourcesOutsideAllowedRoots?: boolean; allowReservedContainerTargets?: boolean; + allowContainerNamespaceJoin?: boolean; }) { // Runtime security validation: blocks dangerous bind mounts, network modes, and profiles. validateSandboxSecurity({ @@ -278,6 +279,9 @@ export function buildSandboxCreateArgs(params: { allowReservedContainerTargets: params.allowReservedContainerTargets ?? params.cfg.dangerouslyAllowReservedContainerTargets === true, + dangerouslyAllowContainerNamespaceJoin: + params.allowContainerNamespaceJoin ?? + params.cfg.dangerouslyAllowContainerNamespaceJoin === true, }); const createdAtMs = params.createdAtMs ?? Date.now(); diff --git a/src/agents/sandbox/validate-sandbox-security.test.ts b/src/agents/sandbox/validate-sandbox-security.test.ts index 22a5be14d5d..cc3bd2e00a7 100644 --- a/src/agents/sandbox/validate-sandbox-security.test.ts +++ b/src/agents/sandbox/validate-sandbox-security.test.ts @@ -222,6 +222,30 @@ describe("validateNetworkMode", () => { expect(() => validateNetworkMode(testCase.mode), testCase.mode).toThrow(testCase.expected); } }); + + it("blocks container namespace joins by default", () => { + const cases = [ + { + mode: "container:abc123", + expected: /network mode "container:abc123" is blocked by default/, + }, + { + mode: "CONTAINER:ABC123", + expected: /network mode "CONTAINER:ABC123" is blocked by default/, + }, + ] as const; + for (const testCase of cases) { + expect(() => validateNetworkMode(testCase.mode), testCase.mode).toThrow(testCase.expected); + } + }); + + it("allows container namespace joins with explicit dangerous override", () => { + expect(() => + validateNetworkMode("container:abc123", { + allowContainerNamespaceJoin: true, + }), + ).not.toThrow(); + }); }); describe("validateSeccompProfile", () => { diff --git a/src/agents/sandbox/validate-sandbox-security.ts b/src/agents/sandbox/validate-sandbox-security.ts index 393d9f4b336..928459836c4 100644 --- a/src/agents/sandbox/validate-sandbox-security.ts +++ b/src/agents/sandbox/validate-sandbox-security.ts @@ -42,6 +42,10 @@ export type ValidateBindMountsOptions = { allowReservedContainerTargets?: boolean; }; +export type ValidateNetworkModeOptions = { + allowContainerNamespaceJoin?: boolean; +}; + export type BlockedBindReason = | { kind: "targets"; blockedPath: string } | { kind: "covers"; blockedPath: string } @@ -276,14 +280,30 @@ export function validateBindMounts( } } -export function validateNetworkMode(network: string | undefined): void { - if (network && BLOCKED_NETWORK_MODES.has(network.trim().toLowerCase())) { +export function validateNetworkMode( + network: string | undefined, + options?: ValidateNetworkModeOptions, +): void { + const normalized = network?.trim().toLowerCase(); + if (!normalized) { + return; + } + + if (BLOCKED_NETWORK_MODES.has(normalized)) { throw new Error( `Sandbox security: network mode "${network}" is blocked. ` + 'Network "host" mode bypasses container network isolation. ' + 'Use "bridge" or "none" instead.', ); } + + if (normalized.startsWith("container:") && options?.allowContainerNamespaceJoin !== true) { + throw new Error( + `Sandbox security: network mode "${network}" is blocked by default. ` + + 'Network "container:*" joins another container namespace and bypasses sandbox network isolation. ' + + "Use a custom bridge network, or set dangerouslyAllowContainerNamespaceJoin=true only when you fully trust this runtime.", + ); + } } export function validateSeccompProfile(profile: string | undefined): void { @@ -312,10 +332,13 @@ export function validateSandboxSecurity( network?: string; seccompProfile?: string; apparmorProfile?: string; + dangerouslyAllowContainerNamespaceJoin?: boolean; } & ValidateBindMountsOptions, ): void { validateBindMounts(cfg.binds, cfg); - validateNetworkMode(cfg.network); + validateNetworkMode(cfg.network, { + allowContainerNamespaceJoin: cfg.dangerouslyAllowContainerNamespaceJoin === true, + }); validateSeccompProfile(cfg.seccompProfile); validateApparmorProfile(cfg.apparmorProfile); } diff --git a/src/config/config.sandbox-docker.test.ts b/src/config/config.sandbox-docker.test.ts index d7c3cd286a0..032a68e857b 100644 --- a/src/config/config.sandbox-docker.test.ts +++ b/src/config/config.sandbox-docker.test.ts @@ -53,6 +53,37 @@ describe("sandbox docker config", () => { expect(res.ok).toBe(false); }); + it("rejects container namespace join by default", () => { + const res = validateConfigObject({ + agents: { + defaults: { + sandbox: { + docker: { + network: "container:peer", + }, + }, + }, + }, + }); + expect(res.ok).toBe(false); + }); + + it("allows container namespace join with explicit dangerous override", () => { + const res = validateConfigObject({ + agents: { + defaults: { + sandbox: { + docker: { + network: "container:peer", + dangerouslyAllowContainerNamespaceJoin: true, + }, + }, + }, + }, + }); + expect(res.ok).toBe(true); + }); + it("rejects seccomp unconfined via Zod schema validation", () => { const res = validateConfigObject({ agents: { @@ -219,4 +250,37 @@ describe("sandbox browser binds config", () => { }); expect(res.ok).toBe(false); }); + + it("rejects container namespace join in sandbox.browser config by default", () => { + const res = validateConfigObject({ + agents: { + defaults: { + sandbox: { + browser: { + network: "container:peer", + }, + }, + }, + }, + }); + expect(res.ok).toBe(false); + }); + + it("allows container namespace join in sandbox.browser config with explicit dangerous override", () => { + const res = validateConfigObject({ + agents: { + defaults: { + sandbox: { + docker: { + dangerouslyAllowContainerNamespaceJoin: true, + }, + browser: { + network: "container:peer", + }, + }, + }, + }, + }); + expect(res.ok).toBe(true); + }); }); diff --git a/src/config/types.sandbox.ts b/src/config/types.sandbox.ts index 0d7ecfc8a97..b4d5e6e2027 100644 --- a/src/config/types.sandbox.ts +++ b/src/config/types.sandbox.ts @@ -52,6 +52,11 @@ export type SandboxDockerSettings = { * (workspace + agent workspace roots). */ dangerouslyAllowExternalBindSources?: boolean; + /** + * Dangerous override: allow Docker `network: "container:"` namespace joins. + * Default behavior blocks container namespace joins to preserve sandbox isolation. + */ + dangerouslyAllowContainerNamespaceJoin?: boolean; }; export type SandboxBrowserSettings = { diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index 5147ba576ec..ca559ce5e94 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -126,6 +126,7 @@ export const SandboxDockerSchema = z binds: z.array(z.string()).optional(), dangerouslyAllowReservedContainerTargets: z.boolean().optional(), dangerouslyAllowExternalBindSources: z.boolean().optional(), + dangerouslyAllowContainerNamespaceJoin: z.boolean().optional(), }) .strict() .superRefine((data, ctx) => { @@ -153,7 +154,8 @@ export const SandboxDockerSchema = z } } } - if (data.network?.trim().toLowerCase() === "host") { + const network = data.network?.trim().toLowerCase(); + if (network === "host") { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["network"], @@ -161,6 +163,15 @@ export const SandboxDockerSchema = z 'Sandbox security: network mode "host" is blocked. Use "bridge" or "none" instead.', }); } + if (network?.startsWith("container:") && data.dangerouslyAllowContainerNamespaceJoin !== true) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["network"], + message: + 'Sandbox security: network mode "container:*" is blocked by default. ' + + "Use a custom bridge network, or set dangerouslyAllowContainerNamespaceJoin=true only when you fully trust this runtime.", + }); + } if (data.seccompProfile?.trim().toLowerCase() === "unconfined") { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -464,6 +475,21 @@ export const AgentSandboxSchema = z prune: SandboxPruneSchema, }) .strict() + .superRefine((data, ctx) => { + const browserNetwork = data.browser?.network?.trim().toLowerCase(); + if ( + browserNetwork?.startsWith("container:") && + data.docker?.dangerouslyAllowContainerNamespaceJoin !== true + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["browser", "network"], + message: + 'Sandbox security: browser network mode "container:*" is blocked by default. ' + + "Set sandbox.docker.dangerouslyAllowContainerNamespaceJoin=true only when you fully trust this runtime.", + }); + } + }) .optional(); const CommonToolPolicyFields = { diff --git a/src/security/audit-extra.sync.ts b/src/security/audit-extra.sync.ts index 464930d9126..893d1afb8a0 100644 --- a/src/security/audit-extra.sync.ts +++ b/src/security/audit-extra.sync.ts @@ -830,13 +830,21 @@ export function collectSandboxDangerousConfigFindings(cfg: OpenClawConfig): Secu } const network = typeof docker.network === "string" ? docker.network : undefined; - if (network && network.trim().toLowerCase() === "host") { + const normalizedNetwork = network?.trim().toLowerCase(); + if (normalizedNetwork === "host" || normalizedNetwork?.startsWith("container:")) { + const modeLabel = normalizedNetwork === "host" ? '"host"' : `"${network}"`; + const detail = + normalizedNetwork === "host" + ? `${source}.network is "host" which bypasses container network isolation entirely.` + : `${source}.network is ${modeLabel} which joins another container namespace and can bypass sandbox network isolation.`; 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".`, + title: "Dangerous network mode in sandbox config", + detail, + remediation: + `Set ${source}.network to "bridge", "none", or a custom bridge network name.` + + ` Use ${source}.dangerouslyAllowContainerNamespaceJoin=true only as a break-glass override when you fully trust this runtime.`, }); } diff --git a/src/security/audit.test.ts b/src/security/audit.test.ts index 3b7d54fcb8d..4354c32b77b 100644 --- a/src/security/audit.test.ts +++ b/src/security/audit.test.ts @@ -855,6 +855,31 @@ describe("security audit", () => { ); }); + it("flags container namespace join network mode in sandbox config", async () => { + const cfg: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "all", + docker: { + network: "container:peer", + }, + }, + }, + }, + }; + const res = await audit(cfg); + expect(res.findings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + checkId: "sandbox.dangerous_network_mode", + severity: "critical", + title: "Dangerous network mode in sandbox config", + }), + ]), + ); + }); + it("checks sandbox browser bridge-network restrictions", async () => { const cases: Array<{ name: string;