File Browser: FilePath traversal in download-as-zip/tar via Windows-style backslash separators in stored filenames
Description
Filebrowser on Linux fails to sanitize backslashes in filenames, allowing stored files with literal \ that cause path traversal in archives and arbitrary file write when extracted on Windows.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Filebrowser on Linux fails to sanitize backslashes in filenames, allowing stored files with literal `\` that cause path traversal in archives and arbitrary file write when extracted on Windows.
Vulnerability
Filebrowser versions up to and including v2.63.5 contain a path traversal vulnerability in the archive download feature. In http/raw.go, the getFiles() function builds archive entry names using filepath.ToSlash, which on Linux is a no-op for backslashes. Meanwhile, the resource creation handler in http/resource.go uses path.Clean, which treats only / as a separator, allowing a URL-encoded backslash (%5C) to persist in the stored filename. Any user with the Create permission (default for new users, and signup-enabled instances allow self-registration) can upload a file with a name containing literal backslashes, such as ..\..\..\evil.txt. [1][2]
Exploitation
An attacker with network access and at least the Create permission can: 1) craft a filename containing Windows-style path traversal elements (e.g., ..\..\..\evil.txt) using URL encoding for backslashes; 2) upload it via the resource creation endpoint; 3) the file is stored on the Linux filesystem with a literal \ in its name; 4) when a victim downloads the parent directory as a ZIP or TAR archive, the entry name is emitted verbatim without sanitization; 5) on Windows, standard extractors (Explorer, 7-Zip, WinRAR, .NET ZipFile.ExtractToDirectory) interpret \ as a path separator, causing the file to be written outside the intended extraction folder. [1][2]
Impact
Successfully exploiting this vulnerability allows an attacker to achieve arbitrary file write on any Windows user who downloads and extracts the crafted archive. The attacker controls both the content and the destination path of the written file. This can lead to code execution if the file is placed in a sensitive location such as the Windows startup folder or a system directory. The vulnerability is server-side but the impact is client-side, affecting the extractor's machine. [1][2]
Mitigation
A fix was released in version v2.63.6 (see [4]), which includes commit 847d08b addressing the archive traversal issue among other security fixes. Users should upgrade to v2.63.6 or later immediately. If upgrading is not possible, disabling user registration and restricting the Create permission to trusted users may reduce the attack surface, but these are not complete mitigations. No workaround exists that prevents the exploitation path while maintaining archive download functionality. [3][4]
AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1- Range: <= 1.11.0
Patches
1847d08bdd135fix: address three security disclosures (archive traversal, login DoS, symlink escape)
4 files changed · +77 −1
files/file.go+55 −0 modified@@ -133,6 +133,17 @@ func stat(opts *FileOptions) (*FileInfo, error) { return file, nil } + // The path is a symlink. Refuse to follow it if its on-disk target escapes + // the user's scoped root; otherwise a symlink that lives lexically inside + // the scope but points outside it would let a restricted user read, write, + // or share files beyond their boundary. + if file != nil && file.IsSymlink { + ok, scopeErr := WithinScope(opts.Fs, opts.Path) + if scopeErr != nil || !ok { + return nil, os.ErrPermission + } + } + // fs doesn't support afero.Lstater interface or the file is a symlink info, err := opts.Fs.Stat(opts.Path) if err != nil { @@ -165,6 +176,50 @@ func stat(opts *FileOptions) (*FileInfo, error) { return file, nil } +// WithinScope reports whether the on-disk target of p — after resolving any +// symbolic links — stays within the scoped root of fsys. It exists to stop a +// symlink that lives lexically inside a user's scope but points outside it +// from being followed for reads, writes, or shares. +// +// Paths that do not exist yet (e.g. a brand-new file being created) are +// validated against their nearest existing ancestor, so legitimate new files +// are always allowed. For a filesystem that is not scoped with BasePathFs the +// check is a no-op and returns true. +// +// Note: a dangling symlink whose target does not yet exist resolves to its +// containing directory and is therefore allowed; writing through such a link +// could still create a file outside the scope. Callers that create files +// should treat this as best-effort and rely on rejecting existing escaping +// symlinks, which covers the disclosure and overwrite vectors. +func WithinScope(fsys afero.Fs, p string) (bool, error) { + bfs, ok := fsys.(*afero.BasePathFs) + if !ok { + // Not a scoped filesystem; nothing to enforce. + return true, nil + } + + root, err := filepath.EvalSymlinks(afero.FullBaseFsPath(bfs, "/")) + if err != nil { + return false, err + } + + target := afero.FullBaseFsPath(bfs, p) + resolved, err := filepath.EvalSymlinks(target) + for errors.Is(err, fs.ErrNotExist) { + parent := filepath.Dir(target) + if parent == target { + break + } + target = parent + resolved, err = filepath.EvalSymlinks(target) + } + if err != nil { + return false, err + } + + return resolved == root || strings.HasPrefix(resolved, root+string(filepath.Separator)), nil +} + // Checksum checksums a given File for a given User, using a specific // algorithm. The checksums data is saved on File object. func (i *FileInfo) Checksum(algo string) error {
http/auth.go+9 −1 modified@@ -20,6 +20,8 @@ import ( const ( DefaultTokenExpirationTime = time.Hour * 2 + + maxAuthBodySize = 1 << 20 // 1 MiB ) type userInfo struct { @@ -120,6 +122,10 @@ func withAdmin(fn handleFunc) handleFunc { func loginHandler(tokenExpireTime time.Duration) handleFunc { return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + if r.Body != nil { + r.Body = http.MaxBytesReader(w, r.Body, maxAuthBodySize) + } + auther, err := d.store.Auth.Get(d.settings.AuthMethod) if err != nil { return http.StatusInternalServerError, err @@ -142,7 +148,7 @@ type signupBody struct { Password string `json:"password"` } -var signupHandler = func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) { +var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { if !d.settings.Signup { return http.StatusMethodNotAllowed, nil } @@ -151,6 +157,8 @@ var signupHandler = func(_ http.ResponseWriter, r *http.Request, d *data) (int, return http.StatusBadRequest, nil } + r.Body = http.MaxBytesReader(w, r.Body, maxAuthBodySize) + info := &signupBody{} err := json.NewDecoder(r.Body).Decode(info) if err != nil {
http/raw.go+6 −0 modified@@ -125,6 +125,12 @@ func getFiles(d *data, path, commonPath string) ([]archives.FileInfo, error) { nameInArchive := strings.TrimPrefix(path, commonPath) nameInArchive = strings.TrimPrefix(nameInArchive, string(filepath.Separator)) nameInArchive = filepath.ToSlash(nameInArchive) + // filepath.ToSlash only rewrites the host separator, so on a Linux + // host a stored backslash survives and is emitted verbatim into the + // archive. Windows extractors then treat "\" as a path separator, + // allowing the entry to escape the extraction directory (zip-slip). + // Strip Windows separators regardless of host OS. + nameInArchive = strings.ReplaceAll(nameInArchive, "\\", "/") archiveFiles = append(archiveFiles, archives.FileInfo{ FileInfo: info,
http/resource.go+7 −0 modified@@ -295,6 +295,13 @@ func addVersionSuffix(source string, afs afero.Fs) string { } func writeFile(afs afero.Fs, dst string, in io.Reader, fileMode, dirMode fs.FileMode) (os.FileInfo, error) { + // Refuse to write through a symlink that escapes the user's scope, so an + // overwrite of an existing escaping symlink cannot modify a file outside + // the boundary. + if ok, err := files.WithinScope(afs, dst); err != nil || !ok { + return nil, os.ErrPermission + } + dir, _ := path.Split(dst) err := afs.MkdirAll(dir, dirMode) if err != nil {
Vulnerability mechanics
Synthesis attempt was rejected by the grounding validator. Re-run pending.
References
4News mentions
0No linked articles in our index yet.