Add Docker-based sandbox support with comprehensive security hardening and graceful fallback

Introduce optional Docker sandbox for agent code execution with defense-in-depth security patterns. Add ENABLE_SANDBOX build arg to conditionally install docker-cli in runtime image. Create docker-compose.sandbox.yml overlay with sandbox configuration (512MB memory, 1 CPU, no network, session-scoped containers). Expand shell command deny patterns to cover data exfiltration (DNS tunneling, curl POST), reverse
This commit is contained in:
Viet Tran committed 2026-02-22 19:18:10 +07:00
1 parent 86b1724050
commit 765bec2287
3 files changed
+130 -29

No files matched your search

+8 -2
View File
@@ -32,8 +32,14 @@ RUN set -eux; \
# ── Stage 2: Runtime ──
FROM alpine:3.22
# Install ca-certificates (for HTTPS to LLM APIs) + wget (for healthcheck)
RUN apk add --no-cache ca-certificates wget
ARG ENABLE_SANDBOX=false
# Install ca-certificates + wget (healthcheck) + optionally docker-cli (sandbox)
RUN set -eux; \
apk add --no-cache ca-certificates wget; \
if [ "$ENABLE_SANDBOX" = "true" ]; then \
apk add --no-cache docker-cli; \
fi
# Non-root user
RUN adduser -D -u 1000 -h /app goclaw
+35
View File
@@ -0,0 +1,35 @@
# Sandbox overlay — enables Docker-based sandbox for agent code execution.
#
# Prerequisites:
# 1. Build the sandbox image: docker build -t openclaw-sandbox:bookworm-slim -f Dockerfile.sandbox .
# 2. Ensure Docker socket is accessible
#
# Usage (with managed mode):
# docker compose -f docker-compose.yml -f docker-compose.managed.yml -f docker-compose.sandbox.yml up
#
# SECURITY NOTE: Mounting Docker socket gives the container control over host Docker.
# Only use in trusted environments where agent code execution isolation is required.
services:
goclaw:
build:
args:
ENABLE_SANDBOX: "true"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- GOCLAW_SANDBOX_MODE=all
- GOCLAW_SANDBOX_IMAGE=openclaw-sandbox:bookworm-slim
- GOCLAW_SANDBOX_WORKSPACE_ACCESS=rw
- GOCLAW_SANDBOX_SCOPE=session
- GOCLAW_SANDBOX_MEMORY_MB=512
- GOCLAW_SANDBOX_CPUS=1.0
- GOCLAW_SANDBOX_TIMEOUT_SEC=300
- GOCLAW_SANDBOX_NETWORK=false
# Override base cap_drop to allow Docker socket access
cap_drop: []
cap_add:
- NET_BIND_SERVICE
security_opt: []
group_add:
- ${DOCKER_GID:-999}
+87 -27
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"log/slog"
"os/exec"
"path/filepath"
"regexp"
@@ -13,45 +14,99 @@ import (
)
// Dangerous command patterns to deny by default.
// Defense-in-depth: these patterns complement Docker hardening (cap-drop ALL,
// read-only rootfs, no-new-privileges, pids-limit, memory limit).
// Sources: OWASP Agentic AI Top 10, Claude Code CVE-2025-66032, MITRE ATT&CK,
// PayloadsAllTheThings, Trail of Bits prompt-injection-to-RCE research.
var defaultDenyPatterns = []*regexp.Regexp{
// Destructive file operations
// ── Destructive file operations ──
regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`),
regexp.MustCompile(`\brm\s+.*--recursive`),
regexp.MustCompile(`\brm\s+.*--force`),
regexp.MustCompile(`\bdel\s+/[fq]\b`),
regexp.MustCompile(`\brmdir\s+/s\b`),
regexp.MustCompile(`\b(mkfs|diskpart)\b|\bformat\s`), // "format C:" but not "?format=3"
regexp.MustCompile(`\b(mkfs|diskpart)\b|\bformat\s`),
regexp.MustCompile(`\bdd\s+if=`),
regexp.MustCompile(`>\s*/dev/sd[a-z]\b`),
regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`),
regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`), // fork bomb
// Network exfiltration / remote code execution
regexp.MustCompile(`\bcurl\b.*\|\s*(ba)?sh\b`), // curl | sh
regexp.MustCompile(`\bwget\b.*-O\s*-\s*\|\s*(ba)?sh\b`), // wget -O - | sh
regexp.MustCompile(`/dev/tcp/`), // bash tcp redirect
regexp.MustCompile(`\b(nc|ncat|netcat)\b.*-[el]\b`), // reverse shell listeners
// ── Data exfiltration ──
regexp.MustCompile(`\bcurl\b.*\|\s*(ba)?sh\b`), // curl | sh
regexp.MustCompile(`\bcurl\b.*(-d\b|-F\b|--data|--upload|--form|-T\b|-X\s*P(UT|OST|ATCH))`), // curl POST/PUT
regexp.MustCompile(`\bwget\b.*-O\s*-\s*\|\s*(ba)?sh\b`), // wget | sh
regexp.MustCompile(`\bwget\b.*--post-(data|file)`), // wget POST
regexp.MustCompile(`\b(nslookup|dig|host)\b`), // DNS exfiltration
regexp.MustCompile(`/dev/tcp/`), // bash tcp redirect
// Dangerous eval from untrusted sources
regexp.MustCompile(`\beval\s*\$`), // eval $(...)
regexp.MustCompile(`\bbase64\s+-d\b.*\|\s*(ba)?sh\b`), // base64 -d | sh
// ── Reverse shells ──
regexp.MustCompile(`\b(nc|ncat|netcat)\b.*-[el]\b`),
regexp.MustCompile(`\bsocat\b`),
regexp.MustCompile(`\bopenssl\b.*s_client`),
regexp.MustCompile(`\btelnet\b.*\d+`),
regexp.MustCompile(`\bpython[23]?\b.*\bimport\s+(socket|http\.client|urllib|requests)\b`),
regexp.MustCompile(`\bperl\b.*-e\s*.*\b[Ss]ocket\b`),
regexp.MustCompile(`\bruby\b.*-e\s*.*\b(TCPSocket|Socket)\b`),
regexp.MustCompile(`\bnode\b.*-e\s*.*\b(net\.connect|child_process)\b`),
regexp.MustCompile(`\bawk\b.*/inet/`), // awk built-in networking
regexp.MustCompile(`\bmkfifo\b`), // named pipes for shell redirection
// Bypass-resistant patterns (long flags)
regexp.MustCompile(`\brm\s+.*--recursive`), // rm --recursive (bypasses rm -rf check)
regexp.MustCompile(`\brm\s+.*--force`), // rm --force
// ── Dangerous eval / code injection ──
regexp.MustCompile(`\beval\s*\$`),
regexp.MustCompile(`\bbase64\s+-d\b.*\|\s*(ba)?sh\b`),
// Privilege escalation
regexp.MustCompile(`\bsudo\b`), // sudo
regexp.MustCompile(`\bsu\s+-`), // su - (switch user)
regexp.MustCompile(`\bnsenter\b`), // namespace escape
regexp.MustCompile(`\bunshare\b`), // namespace manipulation
// ── Privilege escalation ──
regexp.MustCompile(`\bsudo\b`),
regexp.MustCompile(`\bsu\s+-`),
regexp.MustCompile(`\bnsenter\b`),
regexp.MustCompile(`\bunshare\b`),
regexp.MustCompile(`\b(mount|umount)\b`),
regexp.MustCompile(`\b(capsh|setcap|getcap)\b`),
// Dangerous path operations on root filesystem
regexp.MustCompile(`\bchmod\s+[0-7]{3,4}\s+/`), // chmod 777 /...
regexp.MustCompile(`\bchown\b.*\s+/`), // chown ... /...
// ── Dangerous path operations ──
regexp.MustCompile(`\bchmod\s+[0-7]{3,4}\s+/`),
regexp.MustCompile(`\bchown\b.*\s+/`),
regexp.MustCompile(`\bchmod\b.*\+x.*/tmp/`), // make tmpfs executable
regexp.MustCompile(`\bchmod\b.*\+x.*/var/tmp/`),
regexp.MustCompile(`\bchmod\b.*\+x.*/dev/shm/`),
// Environment variable injection
regexp.MustCompile(`\bLD_PRELOAD\s*=`), // Linux library injection
regexp.MustCompile(`\bDYLD_INSERT_LIBRARIES\s*=`), // macOS library injection
regexp.MustCompile(`\bLD_LIBRARY_PATH\s*=`), // library path hijack
// ── Environment variable injection ──
regexp.MustCompile(`\bLD_PRELOAD\s*=`),
regexp.MustCompile(`\bDYLD_INSERT_LIBRARIES\s*=`),
regexp.MustCompile(`\bLD_LIBRARY_PATH\s*=`),
regexp.MustCompile(`/etc/ld\.so\.preload`),
// ── Container escape ──
regexp.MustCompile(`/var/run/docker\.sock|docker\.(sock|socket)`),
regexp.MustCompile(`/proc/sys/(kernel|fs|net)/`), // proc writes
regexp.MustCompile(`/sys/(kernel|fs|class|devices)/`), // sysfs manipulation
// ── Crypto mining ──
regexp.MustCompile(`\b(xmrig|cpuminer|minerd|cgminer|bfgminer|ethminer|nbminer|t-rex|phoenixminer|lolminer|gminer|claymore)\b`),
regexp.MustCompile(`stratum\+tcp://|stratum\+ssl://`),
// ── Filter bypass (Claude Code CVE-2025-66032) ──
regexp.MustCompile(`\bsed\b.*['"]/e\b`), // sed /e command execution
regexp.MustCompile(`\bsort\b.*--compress-program`), // sort arbitrary exec
regexp.MustCompile(`\bgit\b.*(--upload-pack|--receive-pack|--exec)=`), // git exec flags
regexp.MustCompile(`\b(rg|grep)\b.*--pre=`), // preprocessor execution
regexp.MustCompile(`\bman\b.*--html=`), // man command injection
regexp.MustCompile(`\bhistory\b.*-[saw]\b`), // history file injection
regexp.MustCompile(`\$\{[^}]*@[PpEeAaKk]\}`), // ${var@P} parameter expansion
// ── Network abuse / reconnaissance ──
regexp.MustCompile(`\b(nmap|masscan|zmap|rustscan)\b`),
regexp.MustCompile(`\b(ssh|scp|sftp)\b.*@`), // outbound SSH
regexp.MustCompile(`\b(chisel|frp|ngrok|cloudflared|bore|localtunnel)\b`), // tunneling tools
// ── Persistence ──
regexp.MustCompile(`\bcrontab\b`),
regexp.MustCompile(`>\s*~/?\.(bashrc|bash_profile|profile|zshrc)`), // shell RC injection
regexp.MustCompile(`\btee\b.*\.(bashrc|bash_profile|profile|zshrc)`),
// ── Process manipulation ──
regexp.MustCompile(`\bkill\s+-9\s`),
regexp.MustCompile(`\b(killall|pkill)\b`),
}
// ExecTool executes shell commands, optionally inside a sandbox container.
@@ -213,9 +268,14 @@ func (t *ExecTool) executeInSandbox(ctx context.Context, command, cwd, sandboxKe
sb, err := t.sandboxMgr.Get(ctx, sandboxKey, t.workingDir)
if err != nil {
if err == sandbox.ErrSandboxDisabled {
return t.executeOnHost(ctx, command, cwd) // fallback to host
return t.executeOnHost(ctx, command, cwd)
}
return ErrorResult(fmt.Sprintf("sandbox error: %v", err))
// Docker unavailable (binary missing, daemon down) → fallback to host
slog.Warn("sandbox unavailable, falling back to host exec",
"error", err,
"command", truncateCmd(command, 80),
)
return t.executeOnHost(ctx, command, cwd)
}
// Map host workdir to container workdir