DOKIMASecurity review report

Overview

tus upload memory-cache eviction logs unescaped host path enabling out-of-band log injection (CWE-117)

low Fully Valid medium confidence

DOK-100131-FILEBROWSER-TUS-UPLOAD-TUS-MEMORY-CACHE-E · filebrowser · tus-upload

Status

Fully Valid

Full attacker-path-to-log-sink chain is complete in source and confirmed against the gorilla/mux dependency on disk (newline survives cleaning, no redirect). Reachable on the default Linux + in-memory configuration with only Perm.Create; low severity (log forgery), but every step is proven. Windows filename semantics scope it out there, noted as a platform limit.

Repository / Component

Repository
filebrowser
Component
tus-upload

Plain-English Description

A user uploading a file can put newline characters into the upload path. If the upload is abandoned, a background cleanup timer later writes that path into the log without escaping it, letting the user inject fake log lines. It only needs upload permission and works on the default setup.

Description of the Underlying Issue

On the default in-memory upload cache (redisURL empty, http/upload_cache_memory.go:80-85), the eviction callback OnEviction fires on expiry and calls fmt.Printf("deleting incomplete upload file: \"%s\"\n", item.Key()) at http/upload_cache_memory.go:41-45, logging the host upload path with an unescaped %s. That key is the file's realpath, registered at http/tus_handlers.go:113 (cache.Register(file.RealPath())) after OpenFile (tus_handlers.go:88) creates a file whose leaf name comes verbatim from the decoded URL path. A percent-encoded control byte (%0A) in the tus URL is decoded by net/http to a literal newline and survives gorilla/mux path cleaning without a redirect (mux cleanPath uses path.Clean, which preserves \n; ServeHTTP only redirects when cleanPath(path)!=path, and http/http.go sets no SkipClean/UseEncodedPath). The rule-based d.Check gate (tus_handlers.go:44) does not reject control bytes. The broken invariant: untrusted path data must be neutralized before reaching a log sink.

Potential Attack

A user with Perm.Create issues POST /api/tus/ghost%0Adeleting%20incomplete%20upload%20file... on Linux/macOS, creating a newline-named file whose realpath is registered in the cache, then abandons the upload. After the ~3-minute TTL, the memory eviction callback logs that realpath with %s, emitting the embedded newline (and any forged trailing content) into the log stream. The trigger is timer-driven and out-of-band from any live request.

Outcomes of Potential Attack

The attacker injects forged/split log records or ANSI/terminal escape sequences into the log, corrupting audit integrity. Because the emission is decoupled from the originating request, the forged line appears with no correlated inbound request, making it harder to attribute. No code execution or data access results.

Affected Scope

http/upload_cache_memory.go:43 (memory-backend eviction fmt.Printf %s of the host upload path); populated via POST /api/tus/<path> (http/tus_handlers.go:88 OpenFile, :113 cache.Register(file.RealPath())). In-memory UploadCache only (default when redisURL is empty).

Suggested Fix (plain english)

Escape the path before logging it in the eviction callback (use %q instead of %s), and reject upload paths containing control characters.

Suggested Fix (detailed)

Change the eviction fmt.Printf at http/upload_cache_memory.go:43 to use %q / strconv.Quote(item.Key()) so control bytes are escaped. Apply the same to the redis backend's path logging and to the request-path logger at http/data.go:71 (also %s). Optionally reject control bytes in tus target paths at the handler (tus_handlers.go:44/88). Verify no upload-cache log emits a realpath with an unescaped %s. Escape before logging: use %q / strconv.Quote(item.Key()) in the eviction fmt.Printf at http/upload_cache_memory.go:43 (Go's %q escapes control bytes). Apply the same neutralization to the redis backend's log paths and to the request-path logger at http/data.go:71 (which also uses %s). Optionally reject control bytes in tus target paths at the handler.

Validation

Full attacker-path-to-log-sink chain is complete in source and confirmed against the gorilla/mux dependency on disk (newline survives cleaning, no redirect). Reachable on the default Linux + in-memory configuration with only Perm.Create; low severity (log forgery), but every step is proven. Windows filename semantics scope it out there, noted as a platform limit.

first_opinion

Full Evidence

http/upload_cache_memory.go:41-45 OnEviction fires on EvictionReasonExpired and calls fmt.Printf("deleting incomplete upload file: \"%s\"\n", item.Key()) — the host path is logged with %s, unescaped, out-of-band from any request
http/tus_handlers.go:88 d.user.Fs.OpenFile(r.URL.Path, O_CREATE|O_WRONLY, ...) creates a file whose leaf name comes verbatim from the decoded URL path; :113 cache.Register(file.RealPath(), uploadLength) stores that host-absolute realpath as the cache key/log value
http/tus_handlers.go:120 the sibling Location header uses r.URL.EscapedPath() (control bytes re-encoded), demonstrating the codebase encodes for the header but not for the eviction log — an inconsistency
/home/ubuntu/go/pkg/mod/github.com/gorilla/mux@v1.8.1/mux.go:464-478 cleanPath uses path.Clean which strips only ./..// and preserves \n; mux.go:176-194 ServeHTTP redirects only when cleanPath(path)!=path, so a newline path is NOT redirected and routes to the handler (http/http.go configures no SkipClean/UseEncodedPath)
http/upload_cache_memory.go:80-85 NewUploadCache returns the memory backend (the Printf sink) whenever redisURL=="" — the default
http/tus_handlers.go:44 gate requires Perm.Create && d.Check(r.URL.Path); a rule-based d.Check (data.go:29-48) does not reject control bytes
Proven fact: A percent-encoded control byte (%0A) in the tus URL path is decoded by net/http to a literal newline in r.URL.Path and survives gorilla/mux path cleaning without a redirect (mux.go cleanPath/ServeHTTP read on disk).
Proven fact: On a filesystem that permits newline in filenames (Linux/macOS), OpenFile at tus_handlers.go:88 succeeds and cache.Register stores a realpath containing the newline.
Proven fact: When the upload is abandoned, the 3-minute TTL eviction callback emits that path via fmt.Printf %s with no escaping, injecting a forged/split log line (CWE-117); the trigger is timer-driven and out-of-band.
Proven fact: The default cache backend is in-memory (redisURL empty), so the vulnerable Printf sink is active by default.
Unvalidated fact: The file-creation step requires an OS that allows newline/control bytes in filenames: Linux and macOS do (and are the primary/Docker deployment), but on Windows OpenFile would fail and Register (tus_handlers.go:113) would not run, so the sink is not reached there. This is a platform scoping limit resolvable from filesystem semantics, not a code gap.
Unvalidated fact: Whether injected ANSI/escape sequences cause terminal spoofing depends on operators viewing logs in a terminal; forged-line spoofing requires a line-oriented log parser — both are deployment consumption facts.
http/upload_cache_memory.go:41-45 OnEviction fires on EvictionReasonExpired and fmt.Printf's item.Key() (host path) with %s, unescaped, out-of-band
http/tus_handlers.go:88 Fs.OpenFile(r.URL.Path,...) creates a file whose leaf name is the decoded URL path; :113 cache.Register(file.RealPath(), ...) stores that realpath as the cache key/log value
http/tus_handlers.go:120 the sibling Location header uses r.URL.EscapedPath() — the codebase encodes for the header but not for the eviction log (inconsistency)
gorilla/mux@v1.8.1/mux.go:464-478 cleanPath uses path.Clean (strips ./..// but preserves \n); mux.go:176-194 ServeHTTP redirects only when cleanPath(path)!=path, so a newline path routes to the handler; http/http.go sets no SkipClean/UseEncodedPath
http/upload_cache_memory.go:80-85 NewUploadCache returns the memory (Printf) backend whenever redisURL=='' — the default
http/tus_handlers.go:44 gate requires Perm.Create && d.Check; rule-based d.Check (data.go:29-48) does not reject control bytes
Proven fact: A percent-encoded control byte (%0A) in the tus URL path decodes to a literal newline in r.URL.Path and survives gorilla/mux cleaning without a redirect (verified against the on-disk mux dependency).
Proven fact: On a filesystem permitting newline filenames, OpenFile at tus_handlers.go:88 succeeds and cache.Register stores a realpath containing the newline.
Proven fact: On upload abandonment the TTL eviction callback emits that path via fmt.Printf %s with no escaping, injecting a forged/split log line; the trigger is timer-driven and out-of-band.
Unvalidated fact: The file-creation step requires an OS that allows newline/control bytes in filenames: Linux and macOS do (primary/Docker deployments); on Windows OpenFile fails and Register never runs, so the sink is not reached there. Platform scoping from filesystem semantics, not a code gap.
http/upload_cache_memory.go:41-45 — OnEviction fmt.Printf("...\"%s\"\n", item.Key()) logs the host path unescaped, then os.Remove
http/tus_handlers.go:88 — d.user.Fs.OpenFile(r.URL.Path, ...) creates a file whose name comes verbatim from the (decoded) URL path
http/tus_handlers.go:113 — cache.Register(file.RealPath(), uploadLength) stores the realpath as the cache key/log value
http/tus_handlers.go:120 — Location header uses r.URL.EscapedPath() (CR/LF encoded) — the codebase encodes for the header but not for the eviction log
files/file.go:209-219 — RealPath returns the BasePathFs host-absolute path including the user-controlled leaf
/home/ubuntu/go/pkg/mod/github.com/gorilla/mux@v1.8.1/mux.go:464-467 (cleanPath removes only ./..//; no SkipClean/UseEncodedPath configured in http/http.go) — a \n in the path survives cleaning without redirect