Overview · Dispositioned issues
Unbounded in-memory materialization of recursive listing in resourceGetRecursiveHandler (availability)
medium Fully Valid high confidence
Status
Fully Valid
Unbounded in-memory materialization plus full-buffer JSON serialization is complete and unconditional in source; authenticated reachability confirmed. A clear resource-abuse/availability issue.
Repository / Component
Plain-English Description
A recursive directory listing loads every file entry in a subtree into memory at once and then makes a second full copy while turning it into JSON, with no limit on count or depth. A user with a large accessible tree can spike server memory and CPU, and repeat the call to exhaust resources.
Description of the Underlying Issue
GET /api/resources/recursive (resourceGetRecursiveHandler, http/resource.go:392-441) walks a subtree with afero.Walk and appends every access-permitted entry into an uncapped []RecursiveEntry (allocated at :407, appended at :427), then calls renderJSON(w, r, entries) at :440. renderJSON (http/utils.go:15-28) calls json.Marshal(data), buffering the full serialized copy in memory (:16) before w.Write (:23) — no streaming. The route (http/http.go:60) is registered with only the logging wrapper: no pagination, entry-count limit, recursion-depth control, or time budget. Any authenticated user (withUser, http/auth.go:83-108) can invoke it, and a Perm.Create user can first inflate their accessible tree to amplify the cost. This is the same availability sink as canonical finding DOK-100066 (afero.Walk appending every entry into an uncapped slice on GET /api/resources/recursive), per the deduplicator.
Potential Attack
An authenticated user (optionally after uploading many files/directories within their scope to enlarge the tree) issues GET /api/resources/recursive against a large directory. The handler materializes every in-scope entry into one uncapped slice and json.Marshal produces a second full byte copy before any bytes are written, spiking heap and CPU for that request. Repeated or concurrent invocations sustain and multiply the memory/CPU pressure.
Outcomes of Potential Attack
Resource-abuse / availability degradation: large transient heap allocation (entries slice plus a full JSON byte copy) and CPU spend on the walk and serialization per request, which repeated/concurrent calls can push toward memory and CPU exhaustion and denial of service for other users. No data disclosure beyond what the caller is already authorized to see.
Affected Scope
GET /api/resources/recursive (resourceGetRecursiveHandler, http/resource.go:392-441); any authenticated user (withUser).
Suggested Fix (plain english)
Cap how many entries and how deep the recursive listing can go, and stream the JSON out instead of building the whole response in memory first.
Suggested Fix (detailed)
In resourceGetRecursiveHandler (http/resource.go:392-441) impose a maximum entry count and/or recursion depth and a time budget; return an error or truncate (with an indicator) beyond a configurable cap, and add pagination. Replace the buffered renderJSON/json.Marshal path with a streaming json.Encoder writing directly to the ResponseWriter so the full response is never held in memory. Verify that a directory far exceeding the cap yields a bounded response and bounded peak memory. Coordinate the fix with canonical finding DOK-100066, which covers the same sink. Impose a maximum entry count and/or recursion depth, stream results with a json.Encoder writing directly to the ResponseWriter instead of buffering via json.Marshal, and add pagination; return an error or truncate beyond a configurable cap.
Validation
Unbounded in-memory materialization plus full-buffer JSON serialization is complete and unconditional in source; authenticated reachability confirmed. A clear resource-abuse/availability issue.
Full Evidence
http/resource.go:392-441 resourceGetRecursiveHandler: afero.Walk over rootPath appends every access-permitted entry into an uncapped []RecursiveEntry (make :407, append :427)
http/resource.go:440 renderJSON(w,r,entries) serializes the entire slice
http/utils.go:15-28 renderJSON: json.Marshal(data) buffers the full serialized copy in memory (:16) then w.Write (:23) - no streaming
http/http.go:60 route registered with only the logging wrapper; no pagination/limit/depth control
http/auth.go:83-108 withUser requires a valid JWT
Proven fact: The handler unconditionally materializes every in-scope entry of the walked subtree into an uncapped slice, then json.Marshal produces a second full in-memory byte copy before writing.
Proven fact: There is no maximum entry count, recursion depth, time budget, or pagination in the handler, its route, or renderJSON.
Proven fact: Reachable by any authenticated user; a Perm.Create user can inflate their own accessible tree first to amplify.
Unvalidated fact: The absolute peak memory/CPU per request scales with the number of files in the user's accessible scope, a runtime/deployment fact; the unbounded materialization itself is unconditional and independent of that value.
http/resource.go:392-441 resourceGetRecursiveHandler: entries := make([]RecursiveEntry, 0) (:407); afero.Walk over rootPath appends every access-permitted entry (append :427); renderJSON(w,r,entries) (:440) — verified in source.
http/utils.go:15-28 renderJSON: json.Marshal(data) buffers the full serialized copy in memory (:16) then w.Write (:23) — no streaming.
http/http.go:60 route registered with only the logging wrapper; no pagination/limit/depth control.
http/auth.go:83-108 withUser requires a valid JWT.
Deduplicator: same sink as canonical DOK-100066-FILEBROWSER-RESOURCE-FILE-CRUD-UNBOUNDED-SERVER-S (dedup_status merged_duplicate, anchor_corroboration).
http/resource.go:392-441 resourceGetRecursiveHandler: afero.Walk over rootPath appends every in-scope entry into an uncapped []RecursiveEntry slice (make at 407, append at 427)
http/resource.go:440 renderJSON serializes the entire slice in one response — no streaming
http/http.go:60 route registered with only the monkey logging wrapper; no pagination/limit/depth control
No max-entries, depth, or time bound anywhere in the handler or its callers