From 765bec228761b6b6b87ca9ac3e07ec2b13541973 Mon Sep 17 00:00:00 2001 From: Viet Tran Date: Sun, 22 Feb 2026 19:18:10 +0700 Subject: [PATCH] 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 --- Dockerfile | 10 +++- docker-compose.sandbox.yml | 35 ++++++++++++ internal/tools/shell.go | 114 ++++++++++++++++++++++++++++--------- 3 files changed, 130 insertions(+), 29 deletions(-) create mode 100644 docker-compose.sandbox.yml diff --git a/Dockerfile b/Dockerfile index b3ac06ee..7413f3b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/docker-compose.sandbox.yml b/docker-compose.sandbox.yml new file mode 100644 index 00000000..1a6883dd --- /dev/null +++ b/docker-compose.sandbox.yml @@ -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} diff --git a/internal/tools/shell.go b/internal/tools/shell.go index 3c33880b..5e7b0cf7 100644 --- a/internal/tools/shell.go +++ b/internal/tools/shell.go @@ -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