DOKIMASecurity review report

Overview

tus PATCH upload copy has no read/idle deadline or concurrency cap, enabling slow-body resource exhaustion

medium Possibly Valid medium confidence

DOK-100052-FILEBROWSER-TUS-UPLOAD-TUS-PATCH-STREAMS · filebrowser · tus-upload

Status

Possibly Valid

The missing read/idle deadline, held fd, and TTL-defeating goroutine are all proven in source; the exhaustion magnitude is gated on host limits and any fronting-proxy body timeout, so possibly_valid. Novel (no decided twin); publish.

Repository / Component

Repository
filebrowser
Component
tus-upload

Plain-English Description

When a logged-in user uploads a file in chunks, the server keeps the connection, an open file, and a background helper alive for as long as the upload lasts, with no time limit and no cap on how many can run at once. A user who opens many deliberately slow uploads can tie up the server's resources.

Description of the Underlying Issue

The tus PATCH handler streams the request body into the target file with io.Copy(openFile, r.Body) (http/tus_handlers.go:218) using no io.LimitReader and no per-connection read or idle deadline. For the whole duration of the copy it holds a second open file descriptor on the destination (opened at http/tus_handlers.go:206 and released only by defer at line 210), and it spawns a per-PATCH background goroutine via keepUploadActive (http/tus_handlers.go:192-193) that touches the upload cache every 2 seconds. Because Touch refreshes the 3-minute TTL (http/upload_cache_memory.go:12,41-53), the cache entry that would otherwise expire and clean up never expires while the connection is held open. The HTTP server sets only ReadHeaderTimeout and no ReadTimeout/IdleTimeout/WriteTimeout (cmd/root.go:246-249), so a stalled body after the headers is never reaped server-side. There is also no per-user concurrency limit on uploads.

Potential Attack

An authenticated user with Perm.Create opens a large number of concurrent PATCH uploads to /api/tus and then sends the request bodies extremely slowly (slowloris-style) or stalls them entirely after the headers. Each connection pins an open file descriptor, a live goroutine, and a cache entry that is continuously refreshed so it never evicts. Because nothing on the server side times out or bounds the number of simultaneous uploads, the attacker accumulates held resources for as long as the connections stay open.

Outcomes of Potential Attack

Progressive exhaustion of server file descriptors, goroutines, and connection slots, degrading or denying service for other users. The resource hold is bounded only by external limits (OS fd/ulimit, connection count) rather than by any application control, so a single low-privilege account can drive resource pressure disproportionate to any legitimate upload workload.

Affected Scope

PATCH /api/tus for any Perm.Create user: a slow/stalled upload body has no read/idle deadline and no concurrency cap, pinning fds, goroutines, and cache entries.

Suggested Fix (plain english)

Give uploads a time limit and a cap.

Suggested Fix (detailed)

Set http.Server ReadTimeout and IdleTimeout (and consider a per-request read deadline via http.ResponseController/SetReadDeadline) in cmd/root.go so post-header slow or stalled bodies are reaped. Wrap the PATCH body with io.LimitReader (or enforce the declared Upload-Length) so a single copy cannot stream unbounded data. Add a per-user concurrent-upload limit and/or a global net.LimitListener so a single account cannot pin an unbounded number of connections/goroutines/fds. Verify by opening many stalled PATCH connections and confirming the server closes idle ones after the timeout and rejects uploads beyond the per-user cap. Add a per-connection read/idle deadline (SetReadDeadline / ReadTimeout / IdleTimeout), cap concurrent uploads per user, and bound the copy with a LimitReader.

Validation

The missing read/idle deadline, held fd, and TTL-defeating goroutine are all proven in source; the exhaustion magnitude is gated on host limits and any fronting-proxy body timeout, so possibly_valid. Novel (no decided twin); publish.

first_opinion

Full Evidence

http/tus_handlers.go:218 io.Copy(openFile, r.Body) with no LimitReader and no read/idle deadline
http/tus_handlers.go:206-210 target-file fd held via defer for the whole copy
http/tus_handlers.go:192-193,20-40 keepUploadActive spawns a per-PATCH goroutine touching the cache every 2s for the connection lifetime
http/upload_cache_memory.go:12,41-53 3-min TTL with OnEviction os.Remove; Touch refreshes TTL so eviction never fires while touched
cmd/root.go:246-249 only ReadHeaderTimeout; no ReadTimeout/IdleTimeout/WriteTimeout
repo-wide: no MaxBytesReader/LimitListener/SetReadDeadline on the HTTP path
Proven fact: The PATCH body copy has no read/idle deadline and no concurrency cap; each connection holds a 2nd fd plus a keepUploadActive goroutine that actively defeats the cache TTL — complete in source
Proven fact: The server sets no ReadTimeout/IdleTimeout to reap slow bodies
Unvalidated fact: Whether slow uploads reach fd/goroutine exhaustion depends on host limits, connection volume, and whether a fronting reverse proxy enforces body-read timeouts — runtime facts
http/tus_handlers.go:218 — io.Copy(openFile, r.Body) with no io.LimitReader and no read/idle deadline (verified in source)
http/tus_handlers.go:206-210 — destination fd opened via OpenFile and held for the whole copy, released only by defer openFile.Close()
http/tus_handlers.go:192-193 — keepUploadActive spawns a per-PATCH goroutine; comment states it prevents the upload from being evicted during the transfer
http/upload_cache_memory.go:12,41-53 — 3-minute TTL with OnEviction os.Remove; Touch refreshes the TTL so eviction never fires while the upload is actively touched
cmd/root.go:246-249 — http.Server sets only ReadHeaderTimeout; no ReadTimeout/IdleTimeout/WriteTimeout
information-results DOK-100049: MaxBytesReader/MaxBytes appears nowhere in the Go codebase; no LimitListener or rate limiter on the HTTP path; shipped product bundles no reverse proxy (docker/alpine/init.sh execs filebrowser directly)
Proven fact: The PATCH body copy uses no LimitReader and no per-connection read/idle deadline (http/tus_handlers.go:218)
Proven fact: Each PATCH holds a second open file descriptor plus a keepUploadActive goroutine that refreshes the cache TTL so the entry does not expire while the connection is held
Proven fact: The HTTP server configures no ReadTimeout/IdleTimeout, so slow/stalled bodies after headers are not reaped server-side (cmd/root.go:246-249)
Proven fact: No per-user concurrency cap on uploads exists in the codebase
Unvalidated fact: Whether slow/stalled uploads actually reach fd, goroutine, or connection exhaustion depends on host OS limits (ulimit), total connection volume, and whether an operator-deployed fronting reverse proxy enforces body-read timeouts — runtime/deployment facts not settleable from source
http/tus_handlers.go:217-221 — io.Copy(openFile, r.Body) with no LimitReader and no read/idle deadline.
http/tus_handlers.go:206-210 — target-file FD held via defer openFile.Close() for the whole copy (a 2nd FD per connection).
http/tus_handlers.go:192-193 & 20-40 — keepUploadActive spawns a per-PATCH goroutine that Touches the cache every 2s for the connection's lifetime.
http/upload_cache_memory.go:12,41-53 — uploadCacheTTL=3m with OnEviction os.Remove; Touch/Register refresh the TTL, so eviction never fires while touched (redis equivalent: upload_cache_redis.go:75-80).
cmd/root.go:246-249 — http.Server sets only ReadHeaderTimeout:60s (no ReadTimeout/IdleTimeout/WriteTimeout).
Repo-wide grep — no ReadTimeout/WriteTimeout/IdleTimeout/TimeoutHandler/SetReadDeadline/LimitListener/MaxBytesReader in any non-test/non-vendor .go file; only the unrelated img/service.go semaphore, so nothing caps tus body-read duration or concurrent upload connections.
Calibration: .dokima/state candidate-findings.jsonl records an ACCEPTED auth-methods candidate 'Public pre-auth endpoints decode an unbounded, un-timed request body…slow-body connection/goroutine DoS' (medium/high) whose invariant explicitly names 'connections, goroutines, or file descriptors' — same class; the prior cli-bootstrap/http-router-middleware timeout dismissals were routing-to-availability-lens, not rejections.