diff --git a/.env.example b/.env.example index 4b95dc8fc..a91c5f9d7 100644 --- a/.env.example +++ b/.env.example @@ -213,7 +213,14 @@ DOCKER_TLS_VERIFY= DOCKER_CERT_PATH= ## Docker settings inside primary terminal container -DOCKER_INSIDE=true # enable to use docker socket +# SECURITY NOTE: DOCKER_INSIDE=true mounts the host Docker socket into every +# sandbox container so agents can launch sub-containers (Docker-in-Docker). +# This grants any process inside the sandbox unrestricted access to the Docker +# API, which can be exploited via prompt injection to escape the sandbox and +# reach the host (issue #337). Only enable this when DinD is required and you +# understand the risk. Consider using a least-privilege socket proxy such as +# https://github.com/Tecnativa/docker-socket-proxy instead of the raw socket. +DOCKER_INSIDE=false # enable to use docker socket (see security note above; default false) DOCKER_NET_ADMIN=true # enable to use net_admin capability DOCKER_SOCKET=/var/run/docker.sock # path on host machine DOCKER_NETWORK= diff --git a/backend/pkg/docker/client.go b/backend/pkg/docker/client.go index 58c1b9bf3..a4b007db9 100644 --- a/backend/pkg/docker/client.go +++ b/backend/pkg/docker/client.go @@ -111,6 +111,13 @@ func NewDockerClient(ctx context.Context, db database.Querier, cfg *config.Confi socket = getHostDockerSocket(ctx, cli) } inside := cfg.DockerInside + if inside { + logrus.Warn("DOCKER_INSIDE=true: the host Docker socket will be bind-mounted into " + + "every sandbox container. Any process inside the sandbox can use the socket " + + "to ask the host daemon to launch a privileged container, achieving a full " + + "host escape (issue #337). Set DOCKER_INSIDE=false unless DinD is required, " + + "or front the socket with a least-privilege proxy (e.g. Tecnativa/docker-socket-proxy).") + } netName := cfg.DockerNetwork publicIP := cfg.DockerPublicIP defImage := strings.ToLower(cfg.DockerDefaultImage) @@ -283,6 +290,27 @@ func (dc *dockerClient) RunContainer( hostConfig.Binds = append(hostConfig.Binds, fmt.Sprintf("%s:%s", dc.socket, defaultDockerSocketPath)) } + // Defense-in-depth: block setuid/setgid and file-capability escalation + // *within* the container (PR_SET_NO_NEW_PRIVS / no-new-privileges). + // NOTE: this does NOT mitigate the docker.sock host-escape in issue #337 — + // an attacker with socket access can ask the host daemon to spawn a privileged + // container regardless of this flag. The real mitigation for #337 is to avoid + // mounting the raw socket (DOCKER_INSIDE=false) or to front it with a + // least-privilege proxy such as https://github.com/Tecnativa/docker-socket-proxy. + // NOTE: no-new-privileges causes the kernel to ignore setuid-root bits on + // execve, so sudo/su from a non-root uid will not work inside the sandbox. + if !slices.Contains(hostConfig.SecurityOpt, "no-new-privileges:true") { + hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, "no-new-privileges:true") + } + + // Cap fork-bomb / resource-exhaustion risk at a bounded limit. + // 2048 pids is generous for most pentest workloads (nmap, hydra, parallel scans). + // Callers can override by setting hostConfig.PidsLimit before calling RunContainer. + if hostConfig.PidsLimit == nil { + pidsLimit := int64(2048) + hostConfig.PidsLimit = &pidsLimit + } + hostConfig.LogConfig = container.LogConfig{ Type: "json-file", Config: map[string]string{ diff --git a/backend/pkg/tools/tools.go b/backend/pkg/tools/tools.go index 2c72884b6..f190b5f00 100644 --- a/backend/pkg/tools/tools.go +++ b/backend/pkg/tools/tools.go @@ -479,7 +479,22 @@ func (fte *flowToolsExecutor) Prepare(ctx context.Context) error { } } - capAdd := []string{"NET_RAW"} + // Start with capabilities pentest tooling genuinely needs. + // NET_RAW: raw sockets (nmap, ping, packet crafting) + // NET_BIND_SERVICE: bind ports <1024 (reverse shells, Responder, rogue DNS) + // SETUID/SETGID: needed by daemons/tools that drop privileges (setuid DOWN). + // NOTE: RunContainer also sets no-new-privileges:true, which causes the kernel + // to ignore setuid-root bits on execve — sudo/su from a non-root uid will NOT + // work. These caps do not re-enable setuid-root escalation. + // CHOWN/DAC_OVERRIDE/FOWNER: root file-permission overrides (dpkg, package builds) + // KILL: signal other processes inside the container + // SYS_CHROOT: chroot-based isolation within the sandbox + capAdd := []string{ + "NET_RAW", "NET_BIND_SERVICE", + "SETUID", "SETGID", + "CHOWN", "DAC_OVERRIDE", "FOWNER", + "KILL", "SYS_CHROOT", + } if fte.cfg.DockerNetAdmin { capAdd = append(capAdd, "NET_ADMIN") } @@ -495,7 +510,8 @@ func (fte *flowToolsExecutor) Prepare(ctx context.Context) error { Entrypoint: []string{"tail", "-f", "/dev/null"}, }, &container.HostConfig{ - CapAdd: capAdd, + CapDrop: []string{"ALL"}, + CapAdd: capAdd, }, ) if err != nil {