Overview · Dispositioned issues
Public directory shares bypass owner-scope access Rules on shared descendants
medium Possibly Valid high confidence
Status
Possibly Valid
Canonical, best-structured version of the access-Rules-bypass finding (full identity fields, archive scope included). Mechanism fully proven from source; possibly_valid because observable impact is contingent on a descendant deny-Rule configuration that reading cannot settle. Supersedes the listing-only DOK-100047 and the older high-rated duplicate DOK-100092; consistent with the medium severity here rather than high because the bypass is confined to the owner's own scope.
Repository / Component
Plain-English Description
When a user shares a folder with a public link, the access rules that normally block certain sub-folders or files stop being enforced. Anyone with the link can browse, download, or bulk-download items inside the shared folder that the owner had configured to be off-limits.
Description of the Underlying Issue
For a public directory share, withHashFile re-roots the owner's filesystem a second time to the shared directory using afero.NewBasePathFs (http/public.go:68). Every subsequent access check is then evaluated against paths expressed relative to that shared directory (e.g. "/private") rather than the owner-scope absolute path (e.g. "/documents/private"). The Checker.Check implementation (http/data.go:29-48) matches settings.Rules and user.Rules against the path exactly as passed, and rules.Rule.Matches (rules/rules.go:29-44) is a lexical exact-equality or "prefix+/" comparison against the rule's stored absolute owner-scope Path. Because the coordinate system was shifted by the re-root while the Rules were not, a deny Rule keyed on "/documents/private" can no longer match the re-rooted "/private", so Check returns its default allow=true. The invariant that owner-configured deny Rules confine what is reachable is broken for all descendants of a publicly shared directory.
Potential Attack
An owner shares a directory (link.Path="/documents") that contains a descendant guarded by a deny Rule (Path="/documents/private", Allow=false). An anonymous visitor with the public share hash: (1) lists the directory via GET /api/public/share/{hash}, where readListing computes the child path as share-relative "/private" and Check("/private") returns allow=true; (2) drills into it via GET /api/public/share/{hash}/private (filePath="/private"); and (3) bulk-exfiltrates it via GET /api/public/dl/{hash}?files=private (or over the whole directory), where rawDirHandler -> getFiles calls d.Check("/private") on the share-relative archive-walk coordinates (http/raw.go:113) and packages the rule-denied members into the returned zip/tar.
Outcomes of Potential Attack
An unauthenticated attacker reads directory listings, downloads individual files, and bulk-downloads (zip/tar) content the owner explicitly denied via access Rules inside a publicly shared directory. This is information disclosure of files intended to be excluded from the share, confined to the sharing owner's own scope (not a cross-user or cross-tenant escape).
Affected Scope
Unauthenticated public directory-share serving: publicShareHandler JSON listing, publicDlHandler single-file and directory archive (rawDirHandler -> getFiles). Owner-scope only.
Suggested Fix (plain english)
When checking access rules for a public share, evaluate the rules against the file's real full path within the owner's storage, not against the path relative to the shared folder, so that owner-configured deny rules still apply.
Suggested Fix (detailed)
Before invoking Check on the public serving paths, translate the share-relative path back into owner-scope absolute coordinates (join basePath/link.Path with the share-relative subpath, e.g. via afero.FullBaseFsPath on the pre-re-root Fs) and pass that absolute path to Check. Apply this consistently everywhere Check runs on public shares: withHashFile/NewFileInfo (files/file.go:78), readListing (files/file.go:408-411), and getFiles (http/raw.go:113). Alternatively, avoid the second BasePathFs re-root and keep evaluating Rules on owner-scope paths. Verify by configuring a deny Rule on a descendant of a shared directory and confirming the descendant is absent from the JSON listing, returns 403 on direct GET, and is excluded from zip/tar archive downloads. Evaluate Check against owner-scope absolute coordinates (basePath joined with the share-relative subpath, e.g. via afero.FullBaseFsPath) rather than the re-rooted relative path, in withHashFile/NewFileInfo/readListing and getFiles.
Validation
Canonical, best-structured version of the access-Rules-bypass finding (full identity fields, archive scope included). Mechanism fully proven from source; possibly_valid because observable impact is contingent on a descendant deny-Rule configuration that reading cannot settle. Supersedes the listing-only DOK-100047 and the older high-rated duplicate DOK-100092; consistent with the medium severity here rather than high because the bypass is confined to the owner's own scope.
Full Evidence
http/public.go:68 d.user.Fs = afero.NewBasePathFs(d.user.Fs, basePath)
http/public.go:70-77 second NewFileInfo(Checker:d) on the re-rooted Fs with share-relative Path
http/data.go:29-48 Check prefix-matches settings.Rules and user.Rules against the path as given
rules/rules.go:29-44 Rule.Matches exact/prefix string match on owner-scope absolute Path
http/raw.go:113 getFiles d.Check(path) on share-relative coordinates for the archive walk
files/file.go:78,408-411 Check invoked on share-relative opts.Path and readListing fPath
Proven fact: Identical mechanism to DOK-100092, verified end-to-end: BasePathFs re-root (public.go:68) plus Check-on-share-relative-path defeats owner-scope-keyed Rules on descendants of a shared directory.
Proven fact: The bypass reaches the archive path as well (getFiles -> d.Check, raw.go:113), matching this record's expanded scope over the listing-only siblings.
Proven fact: Record carries correct stable-identity fields: weakness_class authorization_or_isolation, finding_nature presence, sink at public.go:68 + data.Check.
Unvalidated fact: Whether a descendant deny Rule under a publicly-shared ancestor is actually configured in a given deployment (determines real-world impact).
Unvalidated fact: Prevalence of such configurations in practice.
http/public.go:68 d.user.Fs = afero.NewBasePathFs(d.user.Fs, basePath) re-roots the owner Fs to the shared directory
http/public.go:70-77 second files.NewFileInfo with Path=filePath (share-relative, e.g. "/private" or "") and Checker:d
files/file.go:78 NewFileInfo calls opts.Checker.Check(opts.Path) on the share-relative path
files/file.go:408-411 readListing computes fPath=path.Join(i.Path,name) (share-relative) and calls checker.Check(fPath)
http/raw.go:113 getFiles calls d.Check(path) on share-relative archive-walk coordinates
http/data.go:29-48 Check prefix-matches d.settings.Rules and d.user.Rules against the path argument exactly as given
rules/rules.go:29-44 Rule.Matches is exact-equality or "prefix+/" string match on the rule's absolute owner-scope Path
users/users.go:93-97 user Fs and Rules live in owner-scope coordinates, so the second re-root shifts the coordinate system while Rules do not follow
Proven fact: The public share handler re-roots the Fs a second time (public.go:68) and then evaluates Checker.Check against paths relative to the shared directory, not the owner scope.
Proven fact: Rule matching is a lexical exact/prefix comparison against the rule's stored absolute owner-scope path (rules.go:29-44); a deny Rule keyed on "/documents/private" cannot match the re-rooted "/private" or "/private/child".
Proven fact: The mismatch is present on all three serving paths: the JSON directory listing, single-file download, and the recursive archive walk (getFiles, raw.go:113).
Proven fact: No compensating re-mapping of the path back to owner-scope coordinates occurs before Check; the vulnerable path is complete and readable end-to-end.
Proven fact: Stable-identity fields are correct: weakness_class authorization_or_isolation, finding_nature presence, sink at public.go:68 re-root plus data.go Check.
Unvalidated fact: Whether any real deployment actually configures a deny Rule (global settings.Rules or an admin-set per-user Rule) on a descendant of a directory that a user then publicly shares.
Unvalidated fact: Prevalence of such descendant-deny-Rule configurations in practice.
http/public.go:70-77 second NewFileInfo with Checker:d on the re-rooted Fs and share-relative Path
http/data.go:29-48 Check prefix-matches Rules against the path argument as given (absolute owner coordinates elsewhere, e.g. raw.go:88 normal browse)
http/raw.go:113 getFiles calls d.Check(path) on share-relative coords for the archive walk
hunting-results shares-public authorization_and_isolation lens: archive path inherits the bypass; flagged for merge