Moderate severityNVD Advisory· Published Mar 23, 2026· Updated Mar 25, 2026
OpenClaw < 2026.3.7 - Sandbox Escape via /acp spawn Command
CVE-2026-27646
Description
OpenClaw versions prior to 2026.3.7 contain a sandbox escape vulnerability in the /acp spawn command that allows authorized sandboxed sessions to initialize host-side ACP runtime. Attackers can bypass sandbox restrictions by invoking the /acp spawn slash-command to cross from sandboxed chat context into host-side ACP session initialization when ACP is enabled.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.3.7 | 2026.3.7 |
Affected products
1Patches
161000b8e4dedfix(acp): block sandboxed slash spawns
5 files changed · +56 −15
CHANGELOG.md+1 −0 modified@@ -158,6 +158,7 @@ Docs: https://docs.openclaw.ai - Discord/thread session lifecycle: reset thread-scoped sessions when a thread is archived so reopening a thread starts fresh without deleting transcript history. Thanks @thewilloftheshadow. - Discord/presence defaults: send an online presence update on ready when no custom presence is configured so bots no longer appear offline by default. Thanks @thewilloftheshadow. - Discord/typing cleanup: stop typing indicators after silent/NO_REPLY runs by marking the run complete before dispatch idle cleanup. Thanks @thewilloftheshadow. +- ACP/sandbox spawn parity: block `/acp spawn` from sandboxed requester sessions with the same host-runtime guard already enforced for `sessions_spawn({ runtime: "acp" })`, preserving non-sandbox ACP flows while closing the command-path policy gap. Thanks @patte. - Discord/config SecretRef typing: align Discord account token config typing with SecretInput so SecretRef tokens typecheck. (#32490) Thanks @scoootscooob. - Discord/voice messages: request upload slots with JSON fetch calls so voice message uploads no longer fail with content-type errors. Thanks @thewilloftheshadow. - Discord/voice decoder fallback: drop the native Opus dependency and use opusscript for voice decoding to avoid native-opus installs. Thanks @thewilloftheshadow.
docs/tools/acp-agents.md+1 −1 modified@@ -252,7 +252,7 @@ ACP sessions currently run on the host runtime, not inside the OpenClaw sandbox. Current limitations: -- If the requester session is sandboxed, ACP spawns are blocked. +- If the requester session is sandboxed, ACP spawns are blocked for both `sessions_spawn({ runtime: "acp" })` and `/acp spawn`. - Error: `Sandboxed sessions cannot spawn ACP sessions because runtime="acp" runs on the host. Use runtime="subagent" from sandboxed sessions.` - `sessions_spawn` with `runtime: "acp"` does not support `sandbox: "require"`. - Error: `sessions_spawn sandbox="require" is unsupported for runtime="acp" because ACP sessions run outside the sandbox. Use runtime="subagent" or sandbox="inherit".`
src/agents/acp-spawn.ts+27 −14 modified@@ -81,6 +81,27 @@ export const ACP_SPAWN_ACCEPTED_NOTE = export const ACP_SPAWN_SESSION_ACCEPTED_NOTE = "thread-bound ACP session stays active after this task; continue in-thread for follow-ups."; +export function resolveAcpSpawnRuntimePolicyError(params: { + cfg: OpenClawConfig; + requesterSessionKey?: string; + requesterSandboxed?: boolean; + sandbox?: SpawnAcpSandboxMode; +}): string | undefined { + const sandboxMode = params.sandbox === "require" ? "require" : "inherit"; + const requesterRuntime = resolveSandboxRuntimeStatus({ + cfg: params.cfg, + sessionKey: params.requesterSessionKey, + }); + const requesterSandboxed = params.requesterSandboxed === true || requesterRuntime.sandboxed; + if (requesterSandboxed) { + return 'Sandboxed sessions cannot spawn ACP sessions because runtime="acp" runs on the host. Use runtime="subagent" from sandboxed sessions.'; + } + if (sandboxMode === "require") { + return 'sessions_spawn sandbox="require" is unsupported for runtime="acp" because ACP sessions run outside the sandbox. Use runtime="subagent" or sandbox="inherit".'; + } + return undefined; +} + type PreparedAcpThreadBinding = { channel: string; accountId: string; @@ -242,7 +263,6 @@ export async function spawnAcpDirect( error: "ACP is disabled by policy (`acp.enabled=false`).", }; } - const sandboxMode = params.sandbox === "require" ? "require" : "inherit"; const streamToParentRequested = params.streamTo === "parent"; const parentSessionKey = ctx.agentSessionKey?.trim(); if (streamToParentRequested && !parentSessionKey) { @@ -251,23 +271,16 @@ export async function spawnAcpDirect( error: 'sessions_spawn streamTo="parent" requires an active requester session context.', }; } - const requesterRuntime = resolveSandboxRuntimeStatus({ + const runtimePolicyError = resolveAcpSpawnRuntimePolicyError({ cfg, - sessionKey: ctx.agentSessionKey, + requesterSessionKey: ctx.agentSessionKey, + requesterSandboxed: ctx.sandboxed, + sandbox: params.sandbox, }); - const requesterSandboxed = ctx.sandboxed === true || requesterRuntime.sandboxed; - if (requesterSandboxed) { - return { - status: "forbidden", - error: - 'Sandboxed sessions cannot spawn ACP sessions because runtime="acp" runs on the host. Use runtime="subagent" from sandboxed sessions.', - }; - } - if (sandboxMode === "require") { + if (runtimePolicyError) { return { status: "forbidden", - error: - 'sessions_spawn sandbox="require" is unsupported for runtime="acp" because ACP sessions run outside the sandbox. Use runtime="subagent" or sandbox="inherit".', + error: runtimePolicyError, }; }
src/auto-reply/reply/commands-acp/lifecycle.ts+8 −0 modified@@ -15,6 +15,7 @@ import { resolveAcpSessionCwd, resolveAcpThreadSessionDetailLines, } from "../../../acp/runtime/session-identifiers.js"; +import { resolveAcpSpawnRuntimePolicyError } from "../../../agents/acp-spawn.js"; import { resolveThreadBindingIntroText, resolveThreadBindingThreadName, @@ -253,6 +254,13 @@ export async function handleAcpSpawnAction( } const spawn = parsed.value; + const runtimePolicyError = resolveAcpSpawnRuntimePolicyError({ + cfg: params.cfg, + requesterSessionKey: params.sessionKey, + }); + if (runtimePolicyError) { + return stopWithText(`⚠️ ${runtimePolicyError}`); + } const agentPolicyError = resolveAcpAgentPolicyError(params.cfg, spawn.agentId); if (agentPolicyError) { return stopWithText(
src/auto-reply/reply/commands-acp.test.ts+19 −0 modified@@ -592,6 +592,25 @@ describe("/acp command", () => { ); }); + it("forbids /acp spawn from sandboxed requester sessions", async () => { + const cfg = { + ...baseCfg, + agents: { + defaults: { + sandbox: { mode: "all" }, + }, + }, + } satisfies OpenClawConfig; + + const result = await runDiscordAcpCommand("/acp spawn codex", cfg); + + expect(result?.reply?.text).toContain("Sandboxed sessions cannot spawn ACP sessions"); + expect(hoisted.requireAcpRuntimeBackendMock).not.toHaveBeenCalled(); + expect(hoisted.ensureSessionMock).not.toHaveBeenCalled(); + expect(hoisted.sessionBindingBindMock).not.toHaveBeenCalled(); + expect(hoisted.callGatewayMock).not.toHaveBeenCalled(); + }); + it("cancels the ACP session bound to the current thread", async () => { mockBoundThreadSession({ state: "running" }); const result = await runThreadAcpCommand("/acp cancel", baseCfg);
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/openclaw/openclaw/commit/61000b8e4ded919ca1a825d4700db4cb3fdc56e3ghsapatchWEB
- github.com/advisories/GHSA-9q36-67vc-rrwgghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-9q36-67vc-rrwgghsathird-party-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-27646ghsaADVISORY
- vulncheck.com/advisories/openclaw-mar-sandbox-escape-via-acp-spawn-commandghsathird-party-advisoryWEB
- github.com/openclaw/openclaw/releases/tag/v2026.3.7ghsaWEB
News mentions
0No linked articles in our index yet.