DOKIMASecurity review report

Overview

Path deny rules match case-sensitively while the OS filesystem resolves case-insensitively, allowing rule bypass

medium Possibly Valid medium confidence

DOK-100096-FILEBROWSER-FILESYSTEM-PATH-RE-DENY-ACCESS-RULE-B · filebrowser · filesystem-path-resolution

Status

Possibly Valid

The case-sensitivity/normalization differential between the rule matcher and the OsFs sink is proven in source. Exploitation is gated on a case-insensitive filesystem plus an admin-configured case-specific deny rule — genuine environmental preconditions filebrowser's supported platforms make reachable but that cannot be settled from source; possibly_valid with those named.

Repository / Component

Repository
filebrowser
Component
filesystem-path-resolution

Plain-English Description

Access rules that block a folder or file compare names letter-for-letter, but on Mac, Windows, and some network drives the filesystem treats different capitalizations as the same file. So a blocked path like /secret can be reached by asking for /Secret.

Description of the Underlying Issue

rules/rules.go compares request paths byte-exactly and case-sensitively: line 34 `if path == r.Path { return true }`, line 43 `return strings.HasPrefix(path, prefix)`, and line 30-31 regex matching that is case-sensitive unless the rule author adds (?i). data.Check (http/data.go:29-48) applies settings.Rules then user.Rules via Matches on the request path — the same string later handed to the filesystem. The sink (users/users.go:96) is afero.NewBasePathFs(afero.NewOsFs(), scope), which delegates to the OS; on macOS (APFS/HFS+), Windows (NTFS), and SMB/exFAT the OS resolves filenames case-insensitively. NewFileInfo (files/file.go:78,82) gates Checker.Check(path) then stats/Opens the identical string with no re-canonicalization, so a casing/normalization differential between the matcher and the sink lets a denied path be reached by a different-cased spelling.

Potential Attack

On a case-insensitive filesystem, an admin configures a user deny rule for /secret. The attacker requests /Secret (or on Windows a trailing-dot /secret., trailing-space /secret , or backslash variant). rules.Matches returns false because the string differs byte-for-byte, Check returns allow, and the OsFs resolves the request to the same underlying file — bypassing the deny rule.

Outcomes of Potential Attack

The attacker reads, writes, or otherwise operates on files the administrator intended to deny via path rules, defeating the rule-based access-control boundary within the user's scope.

Affected Scope

rules.Rule.Matches (rules/rules.go) vs afero OsFs sink (users/users.go:96) for every path-rule-gated file operation via data.Check (http/data.go)

Suggested Fix (plain english)

Normalize the path's capitalization the same way the filesystem does before checking access rules, or document that path rules only work reliably on case-sensitive filesystems.

Suggested Fix (detailed)

Canonicalize the request path to the filesystem's case-folding/normalization semantics before rule evaluation (or consistently fold both rule paths and request paths), so the matcher and the OsFs sink agree. Additionally reject Windows trailing-dot/space and backslash path variants. Document that path rules are only reliable on case-sensitive filesystems. Verify: a deny rule for /secret also blocks /Secret and Windows trailing-dot/backslash variants on the affected platforms. Canonicalize the request path to the filesystem's case-folding/normalization semantics before rule evaluation (or fold both rule paths and request paths consistently), and document that path rules are only reliable on case-sensitive filesystems. Consider rejecting Windows trailing-dot/space and backslash path variants.

Validation

The case-sensitivity/normalization differential between the rule matcher and the OsFs sink is proven in source. Exploitation is gated on a case-insensitive filesystem plus an admin-configured case-specific deny rule — genuine environmental preconditions filebrowser's supported platforms make reachable but that cannot be settled from source; possibly_valid with those named.

first_opinion

Full Evidence

rules/rules.go:34 'if path == r.Path { return true }' and rules/rules.go:43 'return strings.HasPrefix(path, prefix)' — byte-exact, case-sensitive matching
rules/rules.go:30-31 'if r.Regex { return r.Regexp.MatchString(path) }' — Go regexp is case-sensitive unless the rule author adds (?i)
http/data.go:29-48 - Check applies settings.Rules then user.Rules via Matches on the request path, the same string later handed to the filesystem
users/users.go:96 - u.Fs = afero.NewBasePathFs(afero.NewOsFs(), scope); the sink is the OS filesystem, which resolves case-insensitively on macOS (APFS/HFS+), Windows (NTFS) and SMB/exFAT mounts
files/file.go:78,82 - NewFileInfo gate Checker.Check(path) then stat/Open on the identical string; no re-canonicalization between the gate and the sink
Negative evidence: grep of http/, rules/, files/ shows no strings.ToLower/ToUpper/EqualFold applied to a request path before Check (the ToLower hits are unrelated field-name/sort uses)
Proven fact: The rule engine compares request paths byte-exactly (and regex case-sensitively by default).
Proven fact: The same request-path string is passed to the afero OsFs sink with no case/normalization folding in between.
Proven fact: afero OsFs delegates to the OS, which performs case-insensitive filename resolution on macOS, Windows and SMB/exFAT (platforms filebrowser officially supports).
Unvalidated fact: Whether a specific deployment runs on a case-insensitive filesystem (macOS/Windows/SMB/exFAT) — on case-sensitive Linux ext4/xfs the differential does not arise; this is a deployment fact.
Unvalidated fact: Whether an admin has configured a path-based deny rule (Allow=false) whose exact casing/spelling can be varied by the attacker (e.g. request /Secret to reach a rule-denied /secret, or on Windows a trailing-dot/backslash variant).
rules/rules.go:34 `if path == r.Path { return true }` and rules/rules.go:43 `return strings.HasPrefix(path, prefix)` — byte-exact, case-sensitive matching
rules/rules.go:30-31 `if r.Regex { return r.Regexp.MatchString(path) }` — Go regexp case-sensitive unless (?i) added
http/data.go:29-48 — Check applies settings.Rules then user.Rules via Matches on the request path, the same string later handed to the filesystem
users/users.go:96 — u.Fs = afero.NewBasePathFs(afero.NewOsFs(), scope); OS resolves case-insensitively on macOS/Windows/SMB/exFAT
files/file.go:78,82 — NewFileInfo gates Checker.Check(path) then stat/Open on the identical string; no re-canonicalization
Negative evidence: grep of http/, rules/, files/ shows no strings.ToLower/ToUpper/EqualFold applied to a request path before Check
Proven fact: afero OsFs delegates to the OS, which performs case-insensitive filename resolution on macOS, Windows, and SMB/exFAT — platforms filebrowser officially supports.
Unvalidated fact: Whether a specific deployment runs on a case-insensitive filesystem — on case-sensitive Linux ext4/xfs the differential does not arise.
Unvalidated fact: Whether an admin has configured a path-based deny rule (Allow=false) whose exact casing/spelling the attacker can vary.
rules/rules.go:34 'if path == r.Path { return true }' and rules/rules.go:43 'return strings.HasPrefix(path, prefix)' — byte-exact, case-sensitive rule matching
rules/rules.go:30-31 'if r.Regex { return r.Regexp.MatchString(path) }' — Go regexp is case-sensitive unless the user adds (?i); rules/rules.go:24-26 MatchHidden uses exact HasPrefix on filepath.Base
http/data.go:29-48 Check applies settings.Rules then user.Rules via Matches, on the same path string later handed to the filesystem
users/users.go:96 'u.Fs = afero.NewBasePathFs(afero.NewOsFs(), scope)' — the sink is the OS filesystem, which resolves case-insensitively on macOS/Windows/SMB/exFAT
files/file.go:78 NewFileInfo gate 'opts.Checker.Check(opts.Path)' followed by sinks at files/file.go:137 (Fs.Stat), :179 (Checksum Fs.Open), :274 (ReadFile content), :419 (readListing Fs.Stat) on the identical string
Negative evidence: no strings.ToLower/ToUpper/EqualFold is applied to any request path before Check (verified across http/*.go); the prior dismissed concern 'Path normalization / parser differential ... (non-symlink)' only established that Check and the sink receive the identical string, not that the rule engine and the OS interpret that string under the same normalization