DOKIMASecurity review report

Overview

Public directory-share access rules evaluated in wrong path namespace after re-root

medium Possibly Valid medium confidence

DOK-100026-FILEBROWSER-RAW-DOWNLOAD-ARCHI-PUBLIC-SHARE-RE-RO · filebrowser · raw-download-archive

Status

Possibly Valid

The namespace-mismatch control failure is fully code-proven and unconditional, but a real confidentiality breach requires a deny rule to exist on a shared-directory subpath, so possibly_valid with that precondition named — consistent with how the comparable confinement finding DOK-100020 was rated. Canonical for the four re-observations (100031/100047/100065).

Repository / Component

Repository
filebrowser
Component
raw-download-archive

Plain-English Description

When someone shares a folder with a public link, the app is supposed to keep honoring any "block access to this subfolder" rules the owner set. Because the code changes how paths are named once the share opens, those block rules stop matching, so a visitor could reach a subfolder the owner intended to keep private.

Description of the Underlying Issue

In the public share flow, after the share root is validated the filesystem is re-rooted to the shared directory via afero.NewBasePathFs (http/public.go:68). Every subsequent access-rule Check for subpaths then runs against share-relative paths (e.g. "private") while settings/user Rules are stored and compared as owner-scope-absolute paths (e.g. "/shared/private"). Rule matching (rules/rules.go:29-44 via http/data.go Check) is pure string prefix/exact/regex comparison with no rebasing between the two namespaces, so a deny Rule that targets a subpath of a shared directory never matches inside the public handler. The first NewFileInfo (public.go:42-51) correctly checks the share root; only the post-re-root subpath checks (public.go:70-77) are broken. This is an asymmetry with the authenticated raw flow (http/raw.go:88-95), which Checks the scope-absolute r.URL.Path.

Potential Attack

An owner shares directory /shared publicly and separately configures a deny Rule on /shared/private (expecting it to remain hidden). An unauthenticated visitor holding the public share hash requests /api/public/dl/HASH/private (or the archive endpoint). The deny rule is evaluated against the share-relative string "private", never matches the owner-absolute Rule.Path "/shared/private", and the Check silently passes.

Outcomes of Potential Attack

Unauthenticated read/download of files and directory listings under a shared folder that the owner's deny rule was meant to block — a confidentiality breach limited to the contents of subpaths of the publicly shared directory that carry a deny rule.

Affected Scope

Unauthenticated GET /api/public/dl|share of a directory share: access Rules on share subpaths are evaluated in the wrong namespace and silently do not match.

Suggested Fix (plain english)

Inside the public share handlers, check access rules using the owner's full path (with the share folder prefix restored) instead of the shortened path that starts at the shared folder.

Suggested Fix (detailed)

Before calling Check on share subpaths, re-prefix the share base (basePath/link.Path) onto the checked path so Rules are evaluated in the owner-absolute namespace that rules/rules.go expects — mirroring how http/raw.go:88-95 Checks scope-absolute r.URL.Path. Alternatively, normalize Rule paths into the re-rooted namespace before matching. Verify by configuring a deny rule on /shared/private, sharing /shared publicly, and confirming /api/public/dl/HASH/private now returns permission-denied. Add a regression test covering an owner-absolute deny rule reached through a directory share. Evaluate Rules against owner-absolute paths inside share handlers (re-prefix the share base onto the checked path before Check), rather than against re-rooted share-relative paths.

Validation

The namespace-mismatch control failure is fully code-proven and unconditional, but a real confidentiality breach requires a deny rule to exist on a shared-directory subpath, so possibly_valid with that precondition named — consistent with how the comparable confinement finding DOK-100020 was rated. Canonical for the four re-observations (100031/100047/100065).

first_opinion

Full Evidence

http/public.go:68 d.user.Fs=afero.NewBasePathFs(d.user.Fs, basePath) re-roots to the share dir
http/public.go:70-77 second NewFileInfo uses Path=filePath (share-relative) with Checker:d
files/file.go:78,406-410 Check runs on share-relative root and children
http/data.go:29-48 + rules/rules.go:29-44 Check matches the literal path string against owner-absolute Rule.Path with no namespace normalization
http/raw.go:88-95 authenticated flow Checks scope-absolute r.URL.Path (the asymmetry)
http/share.go:156-163 sharePostHandler stores Path with no d.Check
Proven fact: After re-root, all Check calls for share subpaths use share-relative paths while settings/user Rules are owner-scope-absolute, so a deny rule on a share subpath never matches — verified in public.go, file.go, data.go, rules.go
Proven fact: The first NewFileInfo (public.go:44) checks only the share ROOT correctly; subpath checks are the broken ones
Proven fact: Rule matching is pure string prefix/exact/regex with no rebasing
Unvalidated fact: Actual data exposure requires an owner/settings deny rule covering a subpath WITHIN a publicly-shared directory; without such a rule nothing is exposed
http/public.go:68 d.user.Fs=afero.NewBasePathFs(d.user.Fs, basePath) re-roots the filesystem to the share directory
http/public.go:70-77 the second NewFileInfo uses Path=filePath (share-relative) with Checker:d, so subpath Checks run in the re-rooted namespace
files/file.go:78,406-410 NewFileInfo/Check runs Checker.Check against the share-relative root and children
http/data.go Check + rules/rules.go:29-44 match the literal path string against owner-absolute Rule.Path with no namespace normalization or rebasing
http/raw.go:88-95 the authenticated flow Checks scope-absolute r.URL.Path — the asymmetry that proves the intended namespace
http/share.go:156-163 sharePostHandler stores the share Path with no d.Check, so no compensating gate exists at share-creation time
Proven fact: After re-root, all Check calls for share subpaths use share-relative paths while settings/user Rules are owner-scope-absolute, so a deny rule on a share subpath never matches (verified across public.go, file.go, data.go, rules.go)
Proven fact: The first NewFileInfo (public.go:42-51) checks only the share ROOT correctly; the post-re-root subpath checks are the broken ones
Proven fact: Rule matching is pure string prefix/exact/regex comparison with no rebasing between namespaces
Unvalidated fact: Actual data exposure requires an owner or settings deny Rule that covers a subpath WITHIN a publicly shared directory; with no such rule nothing is exposed
http/public.go:68 - d.user.Fs = afero.NewBasePathFs(d.user.Fs, basePath) re-roots to the shared subdirectory
http/public.go:70-77 - NewFileInfo built with Path=filePath (share-root-relative) and Checker:d, so Check runs on share-relative paths
http/raw.go:113 - getFiles gates each member via d.Check(path) where path is share-root-relative for public shares
http/data.go:29-48 and rules/rules.go:29-44 - Check/Rule.Matches compare the raw path string against Rule.Path prefix / Regexp with no namespace normalization
http/raw.go:88-95 and http/resource.go:29 - authenticated flows Check scope-relative r.URL.Path, so the same Rule matches there (the asymmetry that makes the share case a bypass)
http/share.go:156-163 - sharePostHandler stores Path=r.URL.Path with no d.Check, permitting sharing of a rule-denied path
Contradicts assumption RDA-A2 (denied files never enter an archive)