DOKIMASecurity review report

Overview

WebSocket /api/command lacks timeout and kill-on-disconnect, orphaning child processes (availability/resource abuse)

low Possibly Valid medium confidence

DOK-100121-FILEBROWSER-COMMAND-EXECUTION-INTERACTIVE-WS-API · filebrowser · command-execution

Status

Possibly Valid

The missing lifecycle controls (no CommandContext, no timeout, no kill-on-disconnect) are proven complete in source, and the orphaned-process abuse path is coherent. possibly_valid because the DoS impact is contingent on config not in source (a hangable/long-running allowlisted command) and Perm.Execute is already a trusted capability, bounding the real trust delta. Sibling of, and distinct from, DOK-100095.

Repository / Component

Repository
filebrowser
Component
command-execution

Plain-English Description

When a user runs a command over the WebSocket endpoint, the server does not tie the command's lifetime to the connection. If the user disconnects, the command keeps running and its goroutine stays parked, so repeatedly starting and dropping long-running commands can pile up abandoned processes.

Description of the Underlying Issue

The /api/command execution lifecycle (http/commands.go:88-119) starts the child with exec.Command (not exec.CommandContext), with no context tied to r.Context() or the connection, no execution timeout, and no write deadline on the streaming loop. When the client disconnects, conn.WriteMessage fails but the scanner loop (:108-113) only logs the error and keeps scanning; there is no cmd.Process.Kill. cmd.Wait() (:115-117) then blocks with no timeout, leaving the request goroutine parked in Scan()/Wait() until the child exits on its own. The broken invariant: a process and goroutine spawned for a connection must be bounded and cancelled when that connection ends.

Potential Attack

A user with Perm.Execute (EnableExec on) and an allowlisted command that can run long or hang (e.g. tail -f, sleep, ping) opens the WebSocket, sends the command, then disconnects — and repeats. Each abandoned invocation leaves a running child process and a parked goroutine that persist independently of any held connection, so the attacker can fire-and-forget to accumulate them.

Outcomes of Potential Attack

Accumulation of orphaned processes and goroutines that outlive their connections, consuming CPU, memory, file descriptors, and process slots — a gradual resource-exhaustion / denial-of-service condition on the server. The trust delta over ordinary repeated legitimate execution is that the work outlives the disconnect.

Affected Scope

http/commands.go:88-119 — the WebSocket /api/command execution lifecycle: exec.Command (not CommandContext), no execution timeout, no write deadline on the streaming loop, and no Process.Kill on client disconnect or write failure. Protected asset: server-side process/goroutine lifecycle bound to the WS connection.

Suggested Fix (plain english)

Tie the command's lifetime to the connection: run it with a cancellable context and a timeout, and kill the process when the client disconnects or a write fails.

Suggested Fix (detailed)

Start the child with exec.CommandContext derived from r.Context() (or a context cancelled on conn close) plus an execution timeout / WaitDelay. On write error or client disconnect, call cmd.Process.Kill() and drain the pipes; set write deadlines on the streaming conn.WriteMessage calls (http/commands.go:108-117). This is distinct from DOK-100095, which addresses the pre-gate read side (unbounded ReadMessage / missing read deadline). Verify that disconnecting the client promptly terminates the child process. Start the child with exec.CommandContext derived from r.Context() (or a context cancelled on conn close) plus an execution timeout; on write error / client disconnect call cmd.Process.Kill() and drain the pipes; set write deadlines on the streaming WriteMessage calls. Distinct from DOK-100095, which addresses the pre-gate read side (unbounded ReadMessage / missing read deadline).

Validation

The missing lifecycle controls (no CommandContext, no timeout, no kill-on-disconnect) are proven complete in source, and the orphaned-process abuse path is coherent. possibly_valid because the DoS impact is contingent on config not in source (a hangable/long-running allowlisted command) and Perm.Execute is already a trusted capability, bounding the real trust delta. Sibling of, and distinct from, DOK-100095.

first_opinion

Full Evidence

http/commands.go:88 cmd := exec.Command(command[0], command[1:]...) — plain exec.Command, no context for cancellation
http/commands.go:108-113 the scanner streaming loop logs conn.WriteMessage errors (log.Print) and keeps scanning; there is no Process.Kill and no write deadline
http/commands.go:115-117 cmd.Wait() blocks with no timeout; the request goroutine remains parked in s.Scan()/Wait() until the child exits on its own
http/commands.go:64 the exec is gated behind d.server.EnableExec && d.user.Perm.Execute and (:80) an allowlist membership check
no CommandContext / WaitDelay / Process.Kill exists anywhere in http/commands.go (read in full)
Proven fact: The command process is started with exec.Command and is not tied to r.Context() or the connection; on client disconnect conn.WriteMessage fails but the loop only logs and continues, so cmd.Wait() is never short-circuited and the process runs to its own completion.
Proven fact: There is no execution timeout and no write deadline, so a long-running or hanging allowlisted command (e.g. tail -f, sleep, ping) outlives the initiating WebSocket connection.
Proven fact: An attacker can fire-and-forget: open WS, send the command, disconnect, repeat — each abandoned process/goroutine persists, enabling accumulation independent of held connections.
Unvalidated fact: Materiality as a DoS depends on runtime configuration not in source: an admin must have allowlisted a command that can be made to run long or hang for the target user, and OS-level process/resource limits must be loose enough for accumulation to matter.
Unvalidated fact: Perm.Execute is already a privileged capability (the user may run allowlisted shell commands); the security delta of this defect over ordinary repeated legitimate execution is the process outliving the disconnect, whose practical severity is deployment-dependent.
http/commands.go:88 cmd := exec.Command(command[0], command[1:]...) — plain exec.Command, no cancellation context
http/commands.go:108-113 the scanner loop logs conn.WriteMessage errors (log.Print) and keeps scanning; no Process.Kill and no write deadline
http/commands.go:115-117 cmd.Wait() blocks with no timeout; the request goroutine stays parked until the child self-exits
http/commands.go:64 exec gated behind d.server.EnableExec && d.user.Perm.Execute and (:80) an allowlist membership check
Proven fact: There is no execution timeout and no write deadline, so a long-running or hanging allowlisted command outlives the initiating WebSocket connection.
http/commands.go:88 exec.Command(command[0], command[1:]...) — not CommandContext
http/commands.go:108-113 scanner loop logs WriteMessage errors and keeps scanning (no kill)
http/commands.go:115-117 cmd.Wait() blocks with no timeout
grep: no Process.Kill/CommandContext/WaitDelay in http/commands.go
cmd/root.go:248 ReadHeaderTimeout does not apply after WS hijack