DOKIMASecurity review report

Overview

Recursive listing handler omits the root-path authorization Check

low Possibly Valid medium confidence

DOK-100027-FILEBROWSER-RESOURCE-FILE-CRUD-RECURSIVE-LISTING · filebrowser · resource-file-crud

Status

Possibly Valid

The missing root-path authorization Check is a real, proven inconsistency vs every other handler. But the confidentiality impact is contingent: prefix deny rules propagate to children (no leak), so exposure needs an anchored-regex/exact deny rule, and the HideDotfiles vector is the user's own data. possibly_valid, severity low, with the precondition named. Canonical of the pair (100068 identical).

Repository / Component

Repository
filebrowser
Component
resource-file-crud

Plain-English Description

The recursive folder-listing feature skips the access check on the folder you point it at, unlike every other file operation. In certain rule configurations this lets a user list the names, sizes, and dates of items inside a folder they were supposed to be blocked from.

Description of the Underlying Issue

GET /api/resources/recursive stats the target rootPath and walks it without ever calling d.Check(rootPath) (http/resource.go:399-405); it explicitly skips the root entry (line 415) and applies d.Check only per child (line 420). Every other resource handler enforces the root gate through files.NewFileInfo, which calls Checker.Check(opts.Path) (files/file.go:78-80). Because MatchHidden is basename-only (rules/rules.go:24-26) and non-regex deny rules propagate to children while anchored-regex/exact rules need not (rules/rules.go:29-44), a state exists where the root directory is denied but some children are allowed. In that state the missing root Check lets the walk enumerate children the caller should not be able to reach through this directory.

Potential Attack

An admin configures an anchored/exact deny rule such as ^/secret$ that denies /secret but not /secret/child. An authenticated user with Perm.Download requests a recursive listing rooted at /secret. The absent root Check allows the walk to proceed, and each allowed child passes its per-child Check.

Outcomes of Potential Attack

Enumeration of child entries (name, size, modtime) under a directory whose root deny rule was intended to block access — a metadata confidentiality leak. Impact is limited: ordinary prefix deny rules propagate to children and leak nothing, and the HideDotfiles vector only exposes the user's own dotfile-directory children.

Affected Scope

GET /api/resources/recursive: the handler omits the root-path Check that every other handler applies, so a caller can target a denied/hidden directory and enumerate its non-matching children.

Suggested Fix (plain english)

Before walking the folder recursively, run the same access check on the target folder that all other operations use, and refuse the request if the folder is denied.

Suggested Fix (detailed)

Add d.Check(rootPath) at the start of the recursive handler (http/resource.go:~399) and return ErrPermission (403) when it fails, mirroring the root gate in files/file.go:78-80. Keep the existing per-child Check. Verify by defining a deny rule ^/secret$, requesting recursive listing rooted at /secret, and confirming a 403 with no child metadata returned. Apply d.Check(rootPath) before walking (mirror NewFileInfo's root gate) and return ErrPermission when the root is denied.

Validation

The missing root-path authorization Check is a real, proven inconsistency vs every other handler. But the confidentiality impact is contingent: prefix deny rules propagate to children (no leak), so exposure needs an anchored-regex/exact deny rule, and the HideDotfiles vector is the user's own data. possibly_valid, severity low, with the precondition named. Canonical of the pair (100068 identical).

first_opinion

Full Evidence

http/resource.go:399-405 Stats rootPath with no d.Check(rootPath)
http/resource.go:415 skips the root before the per-child check
http/resource.go:420 only per-child d.Check is applied
files/file.go:78-80 NewFileInfo enforces Checker.Check(opts.Path) — the root gate this handler omits
rules/rules.go:24-26 MatchHidden is basename-only; rules/rules.go:29-44 non-regex rules propagate deny to children but anchored regex/exact need not
Proven fact: The recursive handler does not Check the root path, unlike every other resource handler (verified against files/file.go:78)
Proven fact: MatchHidden is basename-only and regex rules can be non-propagating, so parent-denied/child-allowed states exist
Unvalidated fact: A genuine confidentiality leak requires an anchored/exact regex deny rule that denies a directory but not its children; ordinary prefix deny rules propagate to children and leak nothing. The HideDotfiles path only exposes the user's OWN dotfile-directory children (a display preference, not an isolation boundary)
http/resource.go:415 skips the root entry before the per-child check
http/resource.go:420 only the per-child d.Check is applied
files/file.go:78-80 NewFileInfo enforces Checker.Check(opts.Path) — the root gate every other handler applies and this one omits
rules/rules.go:24-26 MatchHidden is basename-only; rules/rules.go:29-44 non-regex rules propagate deny to children but anchored-regex/exact rules need not
Unvalidated fact: A genuine confidentiality leak requires an anchored/exact regex deny rule that denies a directory but not its children; ordinary prefix deny rules propagate to children and leak nothing
Unvalidated fact: The HideDotfiles path only exposes the user's OWN dotfile-directory children (a display preference, not an isolation boundary)
http/resource.go:392-405 resourceGetRecursiveHandler stats rootPath with no d.Check(rootPath)
http/resource.go:415 'if fPath == rootPath { return nil }' skips the root before the per-child check
http/resource.go:420 only per-child d.Check(fPath) is applied
http/data.go:29-48 Check; :30 HideDotfiles gate calls rules.MatchHidden
rules/rules.go:24-26 MatchHidden tests only filepath.Base(path) (basename-only)
rules/rules.go:29-44 non-regex rules prefix-match children but regex/exact rules need not; parent-denied does not imply child-denied
files/file.go:78-80 NewFileInfo enforces Checker.Check(opts.Path) — the root-path gate every other handler relies on and this handler omits
http/http.go:60 route wiring: PathPrefix('/resources/recursive') -> resourceGetRecursiveHandler, GET, authenticated only
PRIOR DISMISSAL (materially updated): dismissed-concerns 'resourceGetRecursiveHandler does not Check the root path before walking' (component filesystem-path-resolution) reasoned 'only the root's own existence/is-dir is observable' — incorrect for HideDotfiles (basename-only) and anchored regex denies, which produce parent-denied/child-allowed and leak child entries