Skip to main content

File Browser CVE-2026-54094

| EUVDEUVD-2026-39503 HIGH
Path Traversal (CWE-22)
2026-06-12 https://github.com/filebrowser/filebrowser GHSA-239w-m3h6-ch8v
7.5
CVSS 3.1 · Vendor: https://github.com/filebrowser/filebrowser
Share

Severity by source

Vendor (https://github.com/filebrowser/filebrowser) PRIMARY
7.5 HIGH
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
vuln.today AI
6.8 MEDIUM

Network-reachable but needs a pre-planted in-scope symlink (AC:H) and a scoped account for the reliable read/write paths (PR:L); both confidentiality and integrity are impacted (overwrite), no availability effect.

3.1 AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N
4.0 AV:N/AC:H/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N

Primary rating from Vendor (https://github.com/filebrowser/filebrowser).

CVSS VectorVendor: https://github.com/filebrowser/filebrowser

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

Lifecycle Timeline

7
Analysis Updated
Jun 25, 2026 - 19:30 vuln.today
v3 (cvss_changed)
Analysis Updated
Jun 25, 2026 - 19:30 vuln.today
v2 (cvss_changed)
Re-analysis Queued
Jun 25, 2026 - 19:22 vuln.today
cvss_changed
Severity Changed
Jun 25, 2026 - 19:22 NVD
MEDIUM HIGH
CVSS changed
Jun 25, 2026 - 19:22 NVD
6.8 (MEDIUM) 7.5 (HIGH)
Source Code Evidence Fetched
Jun 12, 2026 - 22:23 vuln.today
Analysis Generated
Jun 12, 2026 - 22:23 vuln.today

DescriptionCVE.org

Summary

File Browser enforces per-user scope with afero.NewBasePathFs(afero.NewOsFs(), scope), set up in users/users.go. This blocks lexical ../ traversal, but it does not stop the HTTP file handlers from following symbolic links before they open, serve, write, share, or list a file. As a result, a scoped user - and in some cases an unauthenticated public-share recipient - can cross the intended scope boundary by following a symlink whose path is lexically inside their scope but whose target is outside it.

Two distinct shapes are covered here:

  • Variant 1 - symlink as the final path component. A symlink that lives inside the user's scoped tree and points to a file under the server root but outside the scope. The handlers record the symlink (IsSymlink) but then resolve and operate on the target anyway.
  • Variant 2 - file or directory reached through a symlinked ancestor. A regular file requested *through* a symlinked directory.

Read, write (including TUS resumable uploads), share creation, and public-share serving are all affected.

Impact

In a multi-user deployment, if a symlink (a file symlink for Variant 1, or a directory symlink for Variant 2) exists inside a restricted user's scoped tree and resolves to a location outside that scope but reachable by the server process, the boundary can be crossed. Concretely, a user holding only normal File Browser permissions can:

  • Read out-of-scope file contents and metadata via GET /api/raw/{path} and GET /api/resources/{path}.
  • Overwrite an out-of-scope target via POST /api/resources/{path}?override=true.
  • Overwrite or create an out-of-scope target via the TUS resumable upload path: POST /api/tus/{path}?override=true followed by PATCH /api/tus/{path}.
  • Create a public share for an out-of-scope target via POST /api/share/{path}, exposing it through GET /api/public/dl/{hash}.

For Variant 2, the same exposure reaches public-share recipients: a normal public directory share whose subtree contains a linked descendant lets an unauthenticated recipient read regular files behind the link, pull them into the share's archive download, and see the resolved target in directory listings.

This breaks the confidentiality and integrity guarantees that per-user scopes and password/anonymous shares are relied upon to provide, for any data the server process can reach.

Technical details

Users are rooted with afero.NewBasePathFs(afero.NewOsFs(), scope). Base-path rooting blocks lexical ../ traversal but does not prevent ordinary filesystem operations from following a symlink whose path is lexically inside the base.

The metadata layer records symlinks but does not consistently re-check the resolved target against the user's real scope:

  • In files/file.go, stat() calls LstatIfPossible, sets IsSymlink, and only invokes the WithinScope containment check when file.IsSymlink true. For Variant 1, this guard (where present) covers the final-element symlink; on the commit tested for Variant 1 the handler still resolved the target with opts.Fs.Stat(opts.Path) and served it. For Variant 2, LstatIfPossible follows a symlinked *ancestor* and returns the leaf as a regular file (IsSymlink false), so stat() returns early and the scope check never runs at all.
  • readListing in files/file.go follows symlink entries to display the target's metadata.
  • http/raw.go builds a file object for the requested path and serves non-directories; its archive walker getFiles follows symlinks via Stat/Open, pulling linked descendants into archive downloads.
  • http/resource.go writes request bodies with writeFile(d.user.Fs, r.URL.Path, ...), and the destination open follows symlinks.
  • http/tus_handlers.go (tusPostHandler, tusPatchHandler) calls MkdirAll/OpenFile on the request path directly with no containment check. Because a brand-new leaf does not stat an existing file, it skips the scope check entirely.
  • http/share.go stores a share for r.URL.Path without checking that the path is not a symlink escape; http/public.go later serves it for unauthenticated downloads (routed at http/http.go:90-91).
  • http/data.go applies dotfile and rule checks to the request path *string*, but never compares the resolved symlink target against the user's real scope.

Proof of concept

Variant 1 - symlink as final path component

Harness layout: server root is a temp directory; restricted user restricted is scoped to /u1 with create, modify, rename, share, and download permissions; a second scope /u2 holds the outside target /u2/secret.txt containing other-secret; and /u1/link-out is a symlink to /u2/secret.txt.

Confirmed bypasses (route-level tests against the real HTTP handlers):

  • GET /api/raw/link-out200 OK, body contains other-secret from /u2/secret.txt.
  • POST /api/resources/link-out?override=true200 OK, /u2/secret.txt changed to pwn.
  • POST /api/tus/link-out?override=true201, then PATCH /api/tus/link-out204, /u2/secret.txt changed.
  • POST /api/share/link-out200 OK, created a public share whose GET /api/public/dl/{hash} returned a body containing other-secret.

Minimal core of the read proof:

go
root := t.TempDir()
os.MkdirAll(filepath.Join(root, "u1"), 0755)
os.MkdirAll(filepath.Join(root, "u2"), 0755)
os.WriteFile(filepath.Join(root, "u2", "secret.txt"), []byte("other-secret"), 0644)
os.Symlink(filepath.Join(root, "u2", "secret.txt"), filepath.Join(root, "u1", "link-out"))

// restricted is a File Browser user scoped to /u1 with Download permission.
rr := authenticatedRequest(t, restricted, http.MethodGet, "/api/raw/link-out", nil)
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "other-secret") {
    t.Fatalf("raw symlink exposed outside target: status=%d body=%q", rr.Code, rr.Body.String())
}

Variant 2 - file reached through a symlinked ancestor

Authenticated scoped user whose scope contains a directory symlink escape_link -> /srv/users/otheruser:

# The symlink itself is correctly blocked
GET /api/resources/escape_link              -> 403 Forbidden
# A regular file THROUGH the symlinked directory is not
GET /api/resources/escape_link/private.txt  -> 200 OK  {"content":"OTHER_USER_SECRET_DATA=...",...}
GET /api/raw/escape_link/private.txt        -> 200 OK  OTHER_USER_SECRET_DATA=...
# Create/overwrite THROUGH the symlinked directory (TUS)
POST  /api/tus/escape_link/injected.txt  (Upload-Length: 20) -> 201 Created
PATCH /api/tus/escape_link/injected.txt  (Upload-Offset: 0)  -> 204 No Content  (written into /srv/users/otheruser/)

Public directory share for /shared, where /shared/link -> ../private and private/secret.txt lives outside the share:

GET /api/public/dl/<hash>/link/secret.txt     -> 200 OK  symlink-secret
GET /api/public/share/<hash>/link/secret.txt  -> 200 OK  {"path":"/link/secret.txt", ...}

Requesting the whole share as an archive pulls link/secret.txt into the zip, and listing the share root exposes the link entry with its resolved target metadata.

Controls that held

The same harness confirmed that ordinary traversal is still rejected, so this is not generic ../ traversal:

  • GET /api/resources/../u2/secret.txt?checksum=sha256 did not succeed as the restricted user.
  • GET /api/resources/%2e%2e/u2/secret.txt did not succeed (encoded dot-dot).
  • POST /api/resources/../u2/new.txt did not create /u2/new.txt.
  • PATCH /api/resources/own.txt?action=rename&destination=/../u2/moved.txt did not move a file outside scope.

Affected code

users/users.go (scope setup); files/file.go (stat, readListing); http/raw.go (getFiles); http/resource.go (writeFile destination); http/tus_handlers.go (tusPostHandler, tusPatchHandler); http/share.go; http/public.go; http/http.go:90-91 (public routes); http/data.go (string-only path checks).

Remediation

Resolve symlinks and verify that the resolved target remains inside the user's real scoped root before any file operation - serving, sharing, writing, truncating, renaming, copying, or deleting. Specifically:

  • Call WithinScope (which resolves every path component with filepath.EvalSymlinks) for all paths in stat(), not only when the final element is a symlink. This closes the ancestor-symlink gap (Variant 2).
  • Add a WithinScope check before MkdirAll/OpenFile in tusPostHandler and tusPatchHandler, so a not-yet-existing leaf cannot skip containment.
  • Omit entries whose resolved target escapes the scope from readListing, and skip them in getFiles before stat/open/recursion.
  • Apply the same resolved-path check consistently to public share creation and public share serving.
  • As an alternative or defense-in-depth, reject symlinks for file operations unless an explicit administrator option enables them.

Add regression tests covering symlink reads, overwrites, TUS create/write, public shares (download, share-info, listing, and archive read paths), and the existing dot-dot controls - plus a positive test confirming that legitimately in-scope symlinks still resolve.

Limitations and non-claims

  • This is not generic ../ path traversal; dot-dot and encoded dot-dot controls held in the route-level tests.
  • This is not a proxy-auth confusion issue; the proofs use normal authenticated requests for a restricted user (and, for Variant 2's share case, an ordinary public-share recipient).
  • The proofs assume the relevant symlink already exists inside the scoped tree, or that another allowed workflow in the deployment can place it there - for example an SMB/NFS export, a Docker bind-mount, or an admin-created link. Web-UI-only creation of the symlink from scratch was not demonstrated.

AnalysisAI

Symlink-following scope escape in File Browser (filebrowser/filebrowser v2 ≤ 2.63.13) lets a restricted, scoped user-and in the public-share case an unauthenticated recipient-read, overwrite, and share files outside their assigned directory whenever a symlink lexically inside their scope resolves to a target elsewhere on the server. The per-user BasePathFs root blocks lexical ../ traversal but the HTTP handlers dereference symlinks before serving, writing, or sharing, so any file the server process can reach is exposed. Publicly available proof-of-concept exploit code exists in the GHSA advisory; the issue is not in CISA KEV and EPSS is low (0.07%, 23rd percentile).

Technical ContextAI

File Browser is a self-hosted Go web file manager (CPE pkg:go/github.com_filebrowser_filebrowser_v2) that confines each user to a directory using afero.NewBasePathFs(afero.NewOsFs(), scope). BasePathFs only enforces lexical containment-it strips ../ and prefixes the scope root-but it does not call EvalSymlinks, so ordinary OS operations (Stat, Open, OpenFile, MkdirAll) transparently follow symbolic links to targets outside the base. This is the classic CWE-22 path-traversal class, but realized via symlink dereference rather than dot-dot sequences. Two shapes exist: Variant 1, where the final path component is itself a symlink (stat() set IsSymlink but still resolved and served the target), and Variant 2, where a regular file is reached through a symlinked ancestor directory-LstatIfPossible follows the ancestor and reports the leaf as a non-symlink (IsSymlink==false), so stat() returns early and the scope containment check never runs. The same gap spans files/file.go (stat, readListing), http/raw.go (getFiles archive walker), http/resource.go (writeFile), http/tus_handlers.go (TUS create/patch with no containment check), and http/share.go / http/public.go (share creation and unauthenticated public serving).

RemediationAI

Vendor-released patch: upgrade File Browser v2 to 2.63.14 or later, which adds a NewScopedFs wrapper that resolves every path component with filepath.EvalSymlinks and refuses to dereference links escaping the scope across stat, listing, raw/archive, write, TUS, and public-share paths (fix commit 7c2c0a11b31b2bb214d741005a0b02b1764208b3, release https://github.com/filebrowser/filebrowser/releases/tag/v2.63.14). For the legacy v1 module (≤ 1.11.0) no fixed version is identified at time of analysis, so migrate to the patched v2 line. If you cannot upgrade immediately, apply compensating controls: audit each user's scoped tree and remove any existing symlinks that resolve outside the scope (this directly closes the disclosure/overwrite vectors); prevent untrusted creation of symlinks in scoped trees-mount user data with the filesystem nosymfollow option or a no-symlink-capable backend (note: breaks any legitimate in-scope symlink workflows); avoid sharing a host directory across multiple File Browser users via Docker bind-mounts or SMB/NFS exports that let one tenant plant links readable by the server; and disable or tightly restrict public/anonymous shares (POST /api/share, GET /api/public/*) until patched, since Variant 2 exposes unauthenticated recipients. Refer to GHSA-239w-m3h6-ch8v for authoritative guidance.

More in Docker

View all
CVE-2024-55964 CRITICAL POC
9.8 Mar 26

An issue was discovered in Appsmith before 1.52. Rated critical severity (CVSS 9.8), this vulnerability is remotely expl

CVE-2019-5736 HIGH POC
8.6 Feb 11

runc through version 1.0-rc6 (used in Docker before 18.09.2) contains a container escape vulnerability that allows attac

CVE-2023-32077 HIGH POC
7.5 Aug 24

Netmaker makes networks with WireGuard. Rated high severity (CVSS 7.5), this vulnerability is remotely exploitable, no a

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2023-5815 HIGH POC
8.1 Nov 22

The News & Blog Designer Pack - WordPress Blog Plugin - (Blog Post Grid, Blog Post Slider, Blog Post Carousel, Blog Post

CVE-2014-9357 CRITICAL
10.0 Dec 16

Docker 1.3.2 allows remote attackers to execute arbitrary code with root privileges via a crafted (1) image or (2) build

CVE-2026-34156 CRITICAL POC
9.9 Mar 30

Remote code execution in NocoBase Workflow Script Node (npm @nocobase/plugin-workflow-javascript) allows authenticated l

CVE-2019-15752 HIGH POC
7.8 Aug 28

Docker Desktop Community Edition before 2.1.0.1 allows local users to gain privileges by placing a Trojan horse docker-c

CVE-2025-34221 CRITICAL POC
10.0 Sep 29

Vasion Print (formerly PrinterLogic) Virtual Appliance Host prior to version 25.2.169 and Application prior to version 2

CVE-2024-23054 CRITICAL POC
9.8 Feb 05

An issue in Plone Docker Official Image 5.2.13 (5221) open-source software that could allow for remote code execution du

CVE-2025-23211 CRITICAL POC
9.9 Jan 28

Tandoor Recipes is an application for managing recipes, planning meals, and building shopping lists. Rated critical seve

CVE-2026-46339 CRITICAL POC
10.0 May 19

Unauthenticated remote code execution in 9router (npm package) versions 0.4.30 through 0.4.36 allows network-adjacent at

Share

CVE-2026-54094 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy