DOKIMASecurity review report

Overview · Dispositioned issues

Hook auth forks an unbounded, timeout-less external subprocess per login on the public pre-auth endpoint (resource-exhaustion DoS)

low Possibly Valid medium confidence

DOK-100050-FILEBROWSER-AUTH-METHODS-HOOK-AUTH-FORKS-A · filebrowser · auth-methods

Disposition

False positive

Developer triage: auth hook is disabled in our deployments and the login endpoint sits behind an external rate limiter; judged not applicable.

By antony · 2026-07-26T19:19:10Z

Status

Possibly Valid

Code path (timeout-less fork on public pre-auth login) is confirmed; exploitability is gated on the non-default 'hook' auth configuration and hook-hang behavior, so possibly_valid with that precondition named.

Repository / Component

Repository
filebrowser
Component
auth-methods

Plain-English Description

When login is handled by an external hook program, every single login attempt starts a new copy of that program before any password is checked, and there is no time limit on how long it may run. A flood of login requests can pile up processes and exhaust the server.

Description of the Underlying Issue

With auth method = 'hook', HookAuth.Auth (auth/hook.go:38-59) invokes a.RunCommand() at line 56 before any credential validation — the external hook itself decides whether to authenticate, block, or pass. RunCommand ultimately executes the configured command via cmd.Output() (auth/hook.go:106-109) using a plain exec.Command with no exec.CommandContext, no deadline, and no timeout, so each call forks one external process and blocks its goroutine until the process exits. The endpoint POST /api/login is public and unauthenticated (http/http.go:49) and, per related finding DOK-100042, carries no rate limiting. Because the fork happens on every request prior to authentication, an unauthenticated attacker can force unbounded subprocess creation and goroutine accumulation, bounded only by the hook binary's own runtime and OS limits.

Potential Attack

With hook auth configured, an unauthenticated attacker sends a high volume of POST /api/login requests (no valid credentials required, since the fork precedes credential checking). Each request forks the configured hook binary and blocks a server goroutine awaiting its output. If the hook is slow, hangs, or blocks on I/O, the outstanding processes and goroutines accumulate faster than they retire.

Outcomes of Potential Attack

Denial of service through resource exhaustion: accumulation of forked subprocesses and blocked goroutines can exhaust process-table slots, file descriptors, memory, or CPU, degrading or halting the server for legitimate users. The attacker gains no data or privilege; the impact is availability.

Affected Scope

HookAuth.RunCommand (auth/hook.go:106-109), reached pre-credential via public POST /api/login

Suggested Fix (plain english)

Put a time limit on each hook invocation, cap how many can run at once, and rate-limit the login endpoint.

Suggested Fix (detailed)

In auth/hook.go replace exec.Command with exec.CommandContext bound to a context with a sensible timeout (context.WithTimeout) so a hung hook cannot block a goroutine indefinitely, and enforce a concurrency cap (e.g. a semaphore) on simultaneous auth subprocesses. Add per-IP/per-account rate limiting in front of loginHandler (shared remediation with DOK-100042). Verify by configuring a deliberately slow/hanging hook and confirming that concurrent login floods are bounded by the timeout and concurrency cap rather than accumulating processes. Use exec.CommandContext with a bounded timeout, add login rate limiting, and cap concurrent auth subprocesses.

Validation

Code path (timeout-less fork on public pre-auth login) is confirmed; exploitability is gated on the non-default 'hook' auth configuration and hook-hang behavior, so possibly_valid with that precondition named.

first_opinion

Full Evidence

auth/hook.go:109 out, err := cmd.Output() with no exec.CommandContext/timeout, one fork per login
auth/hook.go:39-56 RunCommand runs before any credential validation (the hook decides auth/block/pass)
http/http.go:49 /api/login is public (no withUser); no rate limiter on the HTTP path
Proven fact: Every login request forks an external subprocess with no timeout/context, before credentials are checked, on a public endpoint with no rate limiting
Unvalidated fact: Only reachable when the deployment sets auth method = 'hook' (non-default); forking per login is the designed behavior of hook auth
Unvalidated fact: DoS magnitude depends on the hook binary's own runtime/hang behavior and OS process/goroutine limits — not settleable from this repo's source
auth/hook.go:109 — out, err := cmd.Output() with no exec.CommandContext/timeout, one fork per login
auth/hook.go:56 — HookAuth.Auth calls a.RunCommand() before any credential validation (confirmed on disk: RunCommand invoked at line 56, prior to using its result)
auth/hook.go:38-59 — RunCommand result drives the auth/block/pass decision, so the fork is inherent to every login attempt
http/http.go:49 — /api/login is public (no withUser); no rate limiter on the HTTP path (see DOK-100042)
Proven fact: Every login request under hook auth forks an external subprocess with no timeout/context, before credentials are checked, on a public endpoint with no rate limiting.
Proven fact: The subprocess is executed via cmd.Output() (exec.Command, not exec.CommandContext), so there is no upper bound on how long a single hook invocation can hold its goroutine.
Unvalidated fact: Only reachable when the deployment sets auth method = 'hook' (non-default); forking per login is the designed behavior of hook auth.
Unvalidated fact: DoS magnitude depends on the hook binary's own runtime/hang behavior and OS process/goroutine limits — not settleable from this repo's source.
auth/hook.go:106-109 — exec.Command(...).Output() with no CommandContext/timeout, invoked once per login
auth/hook.go:39-56 — RunCommand is reached during pre-auth /api/login handling (before credentials are validated)
http/http.go:49 — /api/login is public (no withUser)
grep: no rate limiter / LimitListener / concurrency cap on the HTTP path