Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
28 changes: 28 additions & 0 deletions backend/pkg/docker/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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{
Expand Down
20 changes: 18 additions & 2 deletions backend/pkg/tools/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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 {
Expand Down