DOKIMASecurity review report

Overview

Redis upload cache has no orphan-file reclamation, unlike the memory backend, leaking abandoned tus uploads on disk

medium Possibly Valid high confidence

DOK-100101-FILEBROWSER-TUS-UPLOAD-REDIS-UPLOAD-CACHE · filebrowser · tus-upload

Status

Possibly Valid

The backend asymmetry is unambiguous in source: the memory backend reclaims orphaned upload files on expiry, the Redis backend does not, and POST creates the file up front. Exploitation is gated on the deployment selecting the Redis backend (a config value not settleable from source); possibly_valid with that precondition named.

Repository / Component

Repository
filebrowser
Component
tus-upload

Plain-English Description

When file-uploads are configured to use Redis (for multi-server setups), abandoned or incomplete uploads are never cleaned off disk. The memory-based setup deletes them automatically, but the Redis one has no such cleanup, so leftover files pile up until the disk fills.

Description of the Underlying Issue

redisUploadCache (http/upload_cache_redis.go:14-84) implements only Register/Complete/GetLength/Touch/Close — no OnEviction callback, no keyspace-notification subscriber, and no os.Remove or any cleanup. The memory backend (http/upload_cache_memory.go:41-46) registers cache.OnEviction(...os.Remove(item.Key())) on EvictionReasonExpired with a janitor goroutine, and this is the only on-disk orphan-reclamation mechanism in the codebase. A tus POST (http/tus_handlers.go:88-92,113) always creates the target file on disk with OpenFile(O_CREATE|O_WRONLY) and then Register(realPath, length) before any PATCH data is written. The backend is selected via redis-cache-url (cmd/root.go:172,181-182). Under the Redis backend the key silently expires after the ~3-minute TTL while the file remains, and no periodic/startup sweep reclaims it, so abandoned/incomplete uploads accumulate without bound.

Potential Attack

An attacker (or ordinary careless client) with Perm.Create, on a deployment run with --redis-cache-url, sends many tus POSTs to distinct paths — creating 0-byte or partially-PATCHed files — and abandons them. After the upload TTL the Redis keys expire but the on-disk files are never removed, so repeated abandonment steadily consumes disk space and inodes.

Outcomes of Potential Attack

Unbounded accumulation of orphaned upload files exhausts disk capacity and/or inodes on the affected instance, degrading or denying service (failed writes/uploads) for all users. No data disclosure; the impact is availability.

Affected Scope

redisUploadCache (http/upload_cache_redis.go) in redis-backed (multi-instance) deployments; abandoned/expired tus uploads on disk

Suggested Fix (plain english)

Give the Redis-based upload cache the same automatic cleanup the memory one has, so leftover upload files are deleted when their record expires.

Suggested Fix (detailed)

Give redisUploadCache an equivalent reclamation path: subscribe to Redis keyspace expiration notifications (or run a periodic reconciliation sweep) that os.Remove's the on-disk file when its upload key expires, and/or add a startup/GC sweep of stale partial uploads. This is distinct from the in-memory abandoned-upload finding DOK-100040. Verify: with --redis-cache-url set, an abandoned tus upload's on-disk file is removed after the TTL. Give the Redis backend an equivalent reclamation path: subscribe to Redis keyspace expiration notifications (or run a periodic reconciliation sweep) that os.Remove's the on-disk file when its upload key expires, and/or a startup/GC sweep of stale partial uploads. Distinct from the in-memory abandoned-upload finding DOK-100040.

Validation

The backend asymmetry is unambiguous in source: the memory backend reclaims orphaned upload files on expiry, the Redis backend does not, and POST creates the file up front. Exploitation is gated on the deployment selecting the Redis backend (a config value not settleable from source); possibly_valid with that precondition named.

first_opinion

Full Evidence

http/upload_cache_redis.go:14-84 - redisUploadCache implements only Register/Complete/GetLength/Touch/Close; no OnEviction, no keyspace-notification subscriber, no os.Remove/cleanup
http/upload_cache_memory.go:41-46 - memory backend registers cache.OnEviction(...os.Remove(item.Key())) on EvictionReasonExpired with a janitor (go cache.Start()); this is the ONLY on-disk orphan-reclamation mechanism in the codebase
http/tus_handlers.go:88-92,113 - POST OpenFile(O_CREATE|O_WRONLY) creates the target file on disk (defer Close) and then cache.Register(realPath, length) before any PATCH data is written
cmd/root.go:172,181-182 - fileCache default NoOp; uploadCache selected via redis-cache-url (redisCacheUrl) for multi-instance deployments
Whole-tree grep: the only os.Remove for uploads is upload_cache_memory.go:44; tus_handlers.go:264 RemoveAll is the explicit termination handler, not an automatic orphan sweep
Proven fact: tus POST always creates an on-disk file and registers it with a 3-minute TTL before data is transferred.
Proven fact: On expiry, the memory backend deletes the on-disk file (OnEviction os.Remove); the Redis backend has no eviction callback or any other on-disk cleanup, so the Redis key silently expires while the file remains.
Proven fact: No other periodic/startup sweep reclaims orphaned tus upload files, so under the Redis backend abandoned/incomplete uploads accumulate on disk without bound.
Unvalidated fact: Whether a given deployment sets redis-cache-url (i.e. selects the Redis backend) — a runtime configuration value not settleable from source; under the default memory backend the orphan is reclaimed.
Unvalidated fact: The time-to-impact depends on upload abandonment rate and available disk/inode capacity (runtime facts).
http/upload_cache_redis.go:14-84 — redisUploadCache implements only Register/Complete/GetLength/Touch/Close; no OnEviction, no keyspace-notification subscriber, no os.Remove/cleanup
http/upload_cache_memory.go:41-46 — memory backend registers cache.OnEviction(...os.Remove(item.Key())) on EvictionReasonExpired with a janitor (go cache.Start()) — the only on-disk orphan-reclamation mechanism
http/tus_handlers.go:88-92,113 — POST OpenFile(O_CREATE|O_WRONLY) creates the target file (defer Close) then cache.Register(realPath, length) before any PATCH data is written
cmd/root.go:172,181-182 — fileCache default NoOp; uploadCache selected via redis-cache-url for multi-instance deployments
Proven fact: tus POST always creates an on-disk file and registers it with a ~3-minute TTL before data is transferred.
Proven fact: On expiry the memory backend deletes the on-disk file (OnEviction os.Remove); the Redis backend has no eviction callback or any other on-disk cleanup, so the Redis key silently expires while the file remains.
http/upload_cache_redis.go:14-84 — full redisUploadCache: Register/Complete/GetLength/Touch/Close only; no OnEviction, no Subscribe/keyspace notification, no os.Remove/cleanup
http/upload_cache_memory.go:41-47 — memory backend reclaims via cache.OnEviction(... os.Remove(item.Key())) on EvictionReasonExpired, janitor started go cache.Start(); this is the ONLY orphan-reclamation mechanism in the codebase and it exists only for the memory backend
http/tus_handlers.go:88-92,113 — POST always creates the target file on disk (defer Close only) and cache.Register()s it before any data is written
cmd/root.go:92,181-182 — redis backend selected via redis-cache-url flag for 'multi-instance deployments'; whole-tree grep shows no other os.Remove/RemoveAll/sweep/cleanup of orphaned tus uploads
Dedup basis: the prior DISMISSED 'POST-registers-many-files inode growth DoS' was dismissed specifically because memory-cache OnEviction os.Remove reclaims — that premise is false for the redis backend, materially changing scope/exploitability