Compare commits

..

No commits in common. "e735491c57b12c3b19dd2e4b570723df78f4eb44" and "b0f92dd2d72af8fa6423a9226a4b89ed17f8589b" have entirely different histories.

8 changed files with 42 additions and 39 deletions

View File

@ -2,15 +2,6 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [2.33.9](https://github.com/filebrowser/filebrowser/compare/v2.33.8...v2.33.9) (2025-06-26)
### Bug Fixes
* check exact match on command allow list ([e2e1e49](https://github.com/filebrowser/filebrowser/commit/e2e1e4913085cca8917e0f69171dc28d3c6af1b6))
* remove auth token from /api/command ([d5b39a1](https://github.com/filebrowser/filebrowser/commit/d5b39a14fd3fc0d1c364116b41289484df7c27b2))
* remove unused import ([c232d41](https://github.com/filebrowser/filebrowser/commit/c232d41f903d3026ec290bbe819b6c59a933048e))
### [2.33.8](https://github.com/filebrowser/filebrowser/compare/v2.33.7...v2.33.8) (2025-06-25)
### [2.33.7](https://github.com/filebrowser/filebrowser/compare/v2.33.6...v2.33.7) (2025-06-25)

View File

@ -38,9 +38,6 @@ File Browser is a **create-your-own-cloud-kind** of software where you can insta
| :----------------------: | :----------------------: | :----------------------: |
| ![](./docs/assets/4.jpg) | ![](./docs/assets/5.jpg) | ![](./docs/assets/6.jpg) |
> [!CAUTION]
>
> The **command execution** functionality has been disabled for all existent and new installations by default from version v2.33.8 and onwards, due to continuous and known security vulnerabilities. You should only use this feature if you are aware of all of the security risks involved. For more up to date information, consult issue [#5199](https://github.com/filebrowser/filebrowser/issues/5199).
## Install

View File

@ -1,5 +1,6 @@
import { baseURL } from "@/utils/constants";
import { removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
const ssl = window.location.protocol === "https:";
const protocol = ssl ? "wss:" : "ws:";
@ -10,8 +11,10 @@ export default function command(
onmessage: WebSocket["onmessage"],
onclose: WebSocket["onclose"]
) {
const authStore = useAuthStore();
url = removePrefix(url);
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}`;
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${authStore.jwt}`;
const conn = new window.WebSocket(url);
conn.onopen = () => conn.send(command);

View File

@ -321,10 +321,7 @@ const save = async () => {
.filter((cmd: string) => cmd !== "");
}
}
newSettings.shell = shellValue.value
.trim()
.split(" ")
.filter((s) => s !== "");
newSettings.shell = shellValue.value.split("\n");
if (newSettings.branding.theme !== getTheme()) {
setTheme(newSettings.branding.theme);

View File

@ -6,7 +6,6 @@ import (
"log"
"net/http"
"os/exec"
"slices"
"strings"
"time"
@ -61,16 +60,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
}
}
// Fail fast
if !d.server.EnableExec || !d.user.Perm.Execute {
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil { //nolint:govet
wsErr(conn, r, http.StatusInternalServerError, err)
}
return 0, nil
}
command, name, err := runner.ParseCommand(d.settings, raw)
command, err := runner.ParseCommand(d.settings, raw)
if err != nil {
if err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())); err != nil { //nolint:govet
wsErr(conn, r, http.StatusInternalServerError, err)
@ -78,7 +68,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
return 0, nil
}
if !slices.Contains(d.user.Commands, name) {
if !d.server.EnableExec || !d.user.CanExecute(command[0]) {
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil { //nolint:govet
wsErr(conn, r, http.StatusInternalServerError, err)
}

View File

@ -1,24 +1,33 @@
package runner
import (
"os/exec"
"github.com/filebrowser/filebrowser/v2/settings"
)
// ParseCommand parses the command taking in account if the current
// instance uses a shell to run the commands or just calls the binary
// directly.
func ParseCommand(s *settings.Settings, raw string) (command []string, name string, err error) {
name, args, err := SplitCommandAndArgs(raw)
if err != nil {
return
}
// directyly.
func ParseCommand(s *settings.Settings, raw string) ([]string, error) {
var command []string
if len(s.Shell) == 0 || s.Shell[0] == "" {
command = append(command, name)
cmd, args, err := SplitCommandAndArgs(raw)
if err != nil {
return nil, err
}
_, err = exec.LookPath(cmd)
if err != nil {
return nil, err
}
command = append(command, cmd)
command = append(command, args...)
} else {
command = append(s.Shell, raw) //nolint:gocritic
}
return command, name, nil
return command, nil
}

View File

@ -60,7 +60,7 @@ func (r *Runner) exec(raw, evt, path, dst string, user *users.User) error {
raw = strings.TrimSpace(strings.TrimSuffix(raw, "&"))
}
command, _, err := ParseCommand(r.Settings, raw)
command, err := ParseCommand(r.Settings, raw)
if err != nil {
return err
}

View File

@ -2,6 +2,7 @@ package users
import (
"path/filepath"
"regexp"
"github.com/spf13/afero"
@ -103,3 +104,18 @@ func (u *User) Clean(baseScope string, fields ...string) error {
func (u *User) FullPath(path string) string {
return afero.FullBaseFsPath(u.Fs.(*afero.BasePathFs), path)
}
// CanExecute checks if an user can execute a specific command.
func (u *User) CanExecute(command string) bool {
if !u.Perm.Execute {
return false
}
for _, cmd := range u.Commands {
if regexp.MustCompile(cmd).MatchString(command) {
return true
}
}
return false
}