GitHub Security Advisory: GHSA-ffx7-75gc-jg7c
Package: github.com/filebrowser/filebrowser/v2 (GO)
Severity: MODERATE (CVSS 0.0)
CWE: CWE-190: Integer Overflow or Wraparound
Vulnerable versions: <= 2.61.1
Patched in: unpatched

Summary: File Browser TUS Negative Upload-Length Fires Post-Upload Hooks Prematurely

Details:
## Summary
The TUS resumable upload handler parses the `Upload-Length` header as a signed 64-bit integer without validating that the value is non-negative. When a negative value is supplied (e.g. `-1`), the first PATCH request immediately satisfies the completion condition (`newOffset >= uploadLength` → `0 >= -1`), causing the server to fire `after_upload` exec hooks with a partial or empty file. An authenticated user with upload permission can trigger any configured `after_upload` hook an unlimited number of times for any filename they choose, regardless of whether the file was actually uploaded - with zero bytes written.

## Details

**Affected file:** `http/tus_handlers.go`

**Vulnerable code - POST (register upload):**
```go
func getUploadLength(r *http.Request) (int64, error) {
    uploadOffset, err := strconv.ParseInt(r.Header.Get("Upload-Length"), 10, 64)
    // ← int64: accepts -1, [PII_REDACTED]854775808, etc.
    if err != nil {
        return 0, fmt.Errorf("invalid upload length: %w", err)
    }
    return uploadOffset, nil
}

// In tusPostHandler:
uploadLength, err := getUploadLength(r)   // uploadLength = -1 (attacker-supplied)
cache.Register(file.RealPath(), uploadLength)  // stores -1 as expected size
```

**Vulnerable code - PATCH (write chunk):**
```go
// In tusPatchHandler:
newOffset := uploadOffset + bytesWritten  // 0 + 0 = 0 (empty body)
if newOffset >= uploadLength {            // 0 >= -1 → TRUE immediately!
    cache.Complete(file.RealPath())
    _ = d.RunHook(func() error { return nil }, "upload", r.URL.Path, "", d.user)
    // ← after_upload hook fires with empty or partial file
}
```

**The completion check uses signed comparison.** Any negative `uploadLength` is always less than `newOffset` (which starts at 0), so the hook fires on the very first PATCH regardless of how many bytes were sent.

**Consequence:** An attacker with upload permission can:
1. Initiate a TUS upload for any filename with `Upload-Length: -1`
2. Send a PATCH with an empty body (`Upload-Offset: 0`)
3. `after_upload` hook fires immediately with a 0-byte (or partial) file
4. Repeat indefinitely - each POST+PATCH cycle re-fires the hook

If exec hooks are enabled and perform important operations on uploaded files (virus scanning, image processing, notifications, data pipeline ingestion), they will be triggered with attacker-controlled filenames and empty file contents.

## Demo Server Setup

```bash
docker run -d --name fb-tus \
  -p 8080:80 \
  -v /tmp/fb-tus:/srv \
  -e FB_EXECER=true \
  filebrowser/filebrowser:v2.31.2

ADMIN_TOKEN=$(curl -s -X POST http://localhost:8080/api/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"admin"}')

# Configure a visible after_upload hook
curl -s -X PUT http://localhost:8080/api/settings \
  -H "X-Auth: $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "commands": {
      "after_upload": ["bash -c \"echo HOOK_FIRED: $FILE $(date) >> /tmp/hook_log.txt\""]
    }
  }