fix: add configurable minimum password length (#5225)

This commit is contained in:
Henrique Dias 2025-06-28 10:07:34 +02:00 committed by GitHub
parent 089255997a
commit 464b644adf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 122 additions and 77 deletions

View File

@ -38,10 +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
For information on how to install File Browser, please check [docs/installation.md](./docs/installation.md).

View File

@ -150,7 +150,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
}
if u == nil {
pass, err := users.HashPwd(a.Cred.Password)
pass, err := users.HashAndValidatePwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
if err != nil {
return nil, err
}
@ -186,7 +186,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
// update the password when it doesn't match the current
if p {
pass, err := users.HashPwd(a.Cred.Password)
pass, err := users.HashAndValidatePwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
if err != nil {
return nil, err
}

View File

@ -1,7 +1,6 @@
package auth
import (
"crypto/rand"
"errors"
"net/http"
@ -29,15 +28,14 @@ func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Sett
}
func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv *settings.Server, username string) (*users.User, error) {
const passwordSize = 32
randomPasswordBytes := make([]byte, passwordSize)
_, err := rand.Read(randomPasswordBytes)
const randomPasswordLength = settings.DefaultMinimumPasswordLength + 10
pwd, err := users.RandomPwd(randomPasswordLength)
if err != nil {
return nil, err
}
var hashedRandomPassword string
hashedRandomPassword, err = users.HashPwd(string(randomPasswordBytes))
hashedRandomPassword, err = users.HashAndValidatePwd(pwd, setting.MinimumPasswordLength)
if err != nil {
return nil, err
}

View File

@ -32,6 +32,7 @@ func addConfigFlags(flags *pflag.FlagSet) {
addUserFlags(flags)
flags.BoolP("signup", "s", false, "allow users to signup")
flags.Bool("create-user-dir", false, "generate user's home directory automatically")
flags.Uint("minimum-password-length", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
flags.String("shell", "", "shell command to which other commands should be appended")
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
@ -144,6 +145,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
fmt.Fprintf(w, "Minimum Password Length:\t%d\n", set.MinimumPasswordLength)
fmt.Fprintf(w, "Auth method:\t%s\n", set.AuthMethod)
fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
fmt.Fprintln(w, "\nBranding:")

View File

@ -29,12 +29,13 @@ override the options.`,
authMethod, auther := getAuthentication(flags)
s := &settings.Settings{
Key: generateKey(),
Signup: mustGetBool(flags, "signup"),
CreateUserDir: mustGetBool(flags, "create-user-dir"),
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
AuthMethod: authMethod,
Defaults: defaults,
Key: generateKey(),
Signup: mustGetBool(flags, "signup"),
CreateUserDir: mustGetBool(flags, "create-user-dir"),
MinimumPasswordLength: mustGetUint(flags, "minimum-password-length"),
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
AuthMethod: authMethod,
Defaults: defaults,
Branding: settings.Branding{
Name: mustGetString(flags, "branding.name"),
DisableExternal: mustGetBool(flags, "branding.disableExternal"),

View File

@ -51,6 +51,8 @@ you want to change. Other options will remain unchanged.`,
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
case "create-user-dir":
set.CreateUserDir = mustGetBool(flags, flag.Name)
case "minimum-password-length":
set.MinimumPasswordLength = mustGetUint(flags, flag.Name)
case "branding.name":
set.Branding.Name = mustGetString(flags, flag.Name)
case "branding.color":

View File

@ -365,10 +365,11 @@ func setupLog(logMethod string) {
func quickSetup(flags *pflag.FlagSet, d pythonData) {
set := &settings.Settings{
Key: generateKey(),
Signup: false,
CreateUserDir: false,
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
Key: generateKey(),
Signup: false,
CreateUserDir: false,
MinimumPasswordLength: settings.DefaultMinimumPasswordLength,
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
Defaults: settings.UserDefaults{
Scope: ".",
Locale: "en",
@ -426,12 +427,12 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
if password == "" {
var pwd string
pwd, err = users.RandomPwd()
pwd, err = users.RandomPwd(set.MinimumPasswordLength)
checkErr(err)
log.Println("Randomly generated password for user 'admin':", pwd)
password, err = users.HashPwd(pwd)
password, err = users.HashAndValidatePwd(pwd, set.MinimumPasswordLength)
checkErr(err)
}

View File

@ -21,7 +21,7 @@ var usersAddCmd = &cobra.Command{
checkErr(err)
getUserDefaults(cmd.Flags(), &s.Defaults, false)
password, err := users.HashPwd(args[1])
password, err := users.HashAndValidatePwd(args[1], s.MinimumPasswordLength)
checkErr(err)
user := &users.User{

View File

@ -27,8 +27,10 @@ options you want to change.`,
password := mustGetString(flags, "password")
newUsername := mustGetString(flags, "username")
s, err := d.store.Settings.Get()
checkErr(err)
var (
err error
user *users.User
)
@ -64,7 +66,7 @@ options you want to change.`,
}
if password != "" {
user.Password, err = users.HashPwd(password)
user.Password, err = users.HashAndValidatePwd(password, s.MinimumPasswordLength)
checkErr(err)
}

View File

@ -123,20 +123,21 @@ filebrowser cmds ls
Or you can use the web interface to manage them via **Settings****Global Settings**.
## Command Execution
## Shell commands
> [!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).
Within Filebrowser you can toggle the shell (`< >` icon at the top right) and this will open a shell command window at the bottom of the screen.
Within File Browser you can toggle the shell (`< >` icon at the top right) and this will open a shell command window at the bottom of the screen. This functionality can be turned on using the environment variable `FB_DISABLE_EXEC=false` or the flag `--disable-exec=false`.
**By default no commands are available as the command list is empty**
To enable commands these need to either be done on a per-user basis (including for the Admin user).
By default no commands are available as the command list is empty. To enable commands these need to either be done on a per-user basis (including for the Admin user).
You can do this by adding them in Settings > User Management > (edit user) > Commands or to *apply to all new users created from that point forward* they can be set in Settings > Global Settings
> [!NOTE]
>
> If using a proxy manager then remember to enable websockets support for the Filebrowser proxy
> If using a proxy manager then remember to enable websockets support for the File Browser proxy
> [!NOTE]
>

View File

@ -7,6 +7,7 @@ var (
ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist")
ErrEmptyPassword = errors.New("password is empty")
ErrShortPassword = errors.New("password is too short")
ErrEmptyUsername = errors.New("username is empty")
ErrEmptyRequest = errors.New("empty request")
ErrScopeIsRelative = errors.New("scope is a relative path")

View File

@ -170,6 +170,7 @@
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
"commandsUpdated": "Commands updated!",
"createUserDir": "Auto create user home dir while adding new user",
"minimumPasswordLength": "Minimum password length",
"tusUploads": "Chunked Uploads",
"tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.",
"tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting byte size input or a string like 10MB, 1GB etc.",

View File

@ -1,6 +1,7 @@
interface ISettings {
signup: boolean;
createUserDir: boolean;
minimumPasswordLength: number;
userHomeBasePath: string;
defaults: SettingsDefaults;
rules: any[];

View File

@ -18,14 +18,26 @@
{{ t("settings.createUserDir") }}
</p>
<div>
<p class="small">{{ t("settings.userHomeBasePath") }}</p>
<p>
<label class="small">{{ t("settings.userHomeBasePath") }}</label>
<input
class="input input--block"
type="text"
v-model="settings.userHomeBasePath"
/>
</div>
</p>
<p>
<label for="minimumPasswordLength">{{
t("settings.minimumPasswordLength")
}}</label>
<vue-number-input
controls
v-model.number="settings.minimumPasswordLength"
id="minimumPasswordLength"
:min="1"
/>
</p>
<h3>{{ t("settings.rules") }}</h3>
<p class="small">{{ t("settings.globalRules") }}</p>
@ -229,17 +241,17 @@
</template>
<script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
import { settings as api } from "@/api";
import { enableExec } from "@/utils/constants";
import UserForm from "@/components/settings/UserForm.vue";
import { StatusError } from "@/api/utils";
import Rules from "@/components/settings/Rules.vue";
import Themes from "@/components/settings/Themes.vue";
import UserForm from "@/components/settings/UserForm.vue";
import { useLayoutStore } from "@/stores/layout";
import { enableExec } from "@/utils/constants";
import { getTheme, setTheme } from "@/utils/theme";
import Errors from "@/views/Errors.vue";
import { computed, inject, onBeforeUnmount, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import { StatusError } from "@/api/utils";
import { getTheme, setTheme } from "@/utils/theme";
const error = ref<StatusError | null>(null);
const originalSettings = ref<ISettings | null>(null);

View File

@ -151,7 +151,7 @@ var signupHandler = func(_ http.ResponseWriter, r *http.Request, d *data) (int,
d.settings.Defaults.Apply(user)
pwd, err := users.HashPwd(info.Password)
pwd, err := users.HashAndValidatePwd(info.Password, d.settings.MinimumPasswordLength)
if err != nil {
return http.StatusInternalServerError, err
}

View File

@ -73,6 +73,9 @@ func handle(fn handleFunc, prefix string, store *storage.Storage, server *settin
if status != 0 {
txt := http.StatusText(status)
if status == http.StatusBadRequest && err != nil {
txt += " (" + err.Error() + ")"
}
http.Error(w, strconv.Itoa(status)+" "+txt, status)
return
}

View File

@ -9,28 +9,30 @@ import (
)
type settingsData struct {
Signup bool `json:"signup"`
CreateUserDir bool `json:"createUserDir"`
UserHomeBasePath string `json:"userHomeBasePath"`
Defaults settings.UserDefaults `json:"defaults"`
Rules []rules.Rule `json:"rules"`
Branding settings.Branding `json:"branding"`
Tus settings.Tus `json:"tus"`
Shell []string `json:"shell"`
Commands map[string][]string `json:"commands"`
Signup bool `json:"signup"`
CreateUserDir bool `json:"createUserDir"`
MinimumPasswordLength uint `json:"minimumPasswordLength"`
UserHomeBasePath string `json:"userHomeBasePath"`
Defaults settings.UserDefaults `json:"defaults"`
Rules []rules.Rule `json:"rules"`
Branding settings.Branding `json:"branding"`
Tus settings.Tus `json:"tus"`
Shell []string `json:"shell"`
Commands map[string][]string `json:"commands"`
}
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
data := &settingsData{
Signup: d.settings.Signup,
CreateUserDir: d.settings.CreateUserDir,
UserHomeBasePath: d.settings.UserHomeBasePath,
Defaults: d.settings.Defaults,
Rules: d.settings.Rules,
Branding: d.settings.Branding,
Tus: d.settings.Tus,
Shell: d.settings.Shell,
Commands: d.settings.Commands,
Signup: d.settings.Signup,
CreateUserDir: d.settings.CreateUserDir,
MinimumPasswordLength: d.settings.MinimumPasswordLength,
UserHomeBasePath: d.settings.UserHomeBasePath,
Defaults: d.settings.Defaults,
Rules: d.settings.Rules,
Branding: d.settings.Branding,
Tus: d.settings.Tus,
Shell: d.settings.Shell,
Commands: d.settings.Commands,
}
return renderJSON(w, r, data)
@ -45,6 +47,7 @@ var settingsPutHandler = withAdmin(func(_ http.ResponseWriter, r *http.Request,
d.settings.Signup = req.Signup
d.settings.CreateUserDir = req.CreateUserDir
d.settings.MinimumPasswordLength = req.MinimumPasswordLength
d.settings.UserHomeBasePath = req.UserHomeBasePath
d.settings.Defaults = req.Defaults
d.settings.Rules = req.Rules

View File

@ -125,7 +125,11 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
return http.StatusBadRequest, fbErrors.ErrEmptyPassword
}
req.Data.Password, err = users.HashPwd(req.Data.Password)
if len(req.Data.Password) < int(d.settings.MinimumPasswordLength) {
return http.StatusBadRequest, fbErrors.ErrShortPassword
}
req.Data.Password, err = users.HashAndValidatePwd(req.Data.Password, d.settings.MinimumPasswordLength)
if err != nil {
return http.StatusInternalServerError, err
}
@ -163,7 +167,7 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
}
if req.Data.Password != "" {
req.Data.Password, err = users.HashPwd(req.Data.Password)
req.Data.Password, err = users.HashAndValidatePwd(req.Data.Password, d.settings.MinimumPasswordLength)
} else {
var suser *users.User
suser, err = d.store.Users.Get(d.server.Root, d.raw.(uint))
@ -186,7 +190,11 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
return http.StatusForbidden, nil
}
req.Data.Password, err = users.HashPwd(req.Data.Password)
if len(req.Data.Password) < int(d.settings.MinimumPasswordLength) {
return http.StatusBadRequest, fbErrors.ErrShortPassword
}
req.Data.Password, err = users.HashAndValidatePwd(req.Data.Password, d.settings.MinimumPasswordLength)
if err != nil {
return http.StatusInternalServerError, err
}

View File

@ -10,23 +10,25 @@ import (
)
const DefaultUsersHomeBasePath = "/users"
const DefaultMinimumPasswordLength = 12
// AuthMethod describes an authentication method.
type AuthMethod string
// Settings contain the main settings of the application.
type Settings struct {
Key []byte `json:"key"`
Signup bool `json:"signup"`
CreateUserDir bool `json:"createUserDir"`
UserHomeBasePath string `json:"userHomeBasePath"`
Defaults UserDefaults `json:"defaults"`
AuthMethod AuthMethod `json:"authMethod"`
Branding Branding `json:"branding"`
Tus Tus `json:"tus"`
Commands map[string][]string `json:"commands"`
Shell []string `json:"shell"`
Rules []rules.Rule `json:"rules"`
Key []byte `json:"key"`
Signup bool `json:"signup"`
CreateUserDir bool `json:"createUserDir"`
UserHomeBasePath string `json:"userHomeBasePath"`
Defaults UserDefaults `json:"defaults"`
AuthMethod AuthMethod `json:"authMethod"`
Branding Branding `json:"branding"`
Tus Tus `json:"tus"`
Commands map[string][]string `json:"commands"`
Shell []string `json:"shell"`
Rules []rules.Rule `json:"rules"`
MinimumPasswordLength uint `json:"minimumPasswordLength"`
}
// GetRules implements rules.Provider.

View File

@ -33,6 +33,9 @@ func (s *Storage) Get() (*Settings, error) {
if set.UserHomeBasePath == "" {
set.UserHomeBasePath = DefaultUsersHomeBasePath
}
if set.MinimumPasswordLength == 0 {
set.MinimumPasswordLength = DefaultMinimumPasswordLength
}
if set.Tus == (Tus{}) {
set.Tus = Tus{
ChunkSize: DefaultTusChunkSize,

View File

@ -5,10 +5,18 @@ import (
"encoding/base64"
"golang.org/x/crypto/bcrypt"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
)
// randomPasswordBytesCount is chosen to fit in a base64 string without padding
const randomPasswordBytesCount = 9
// HashPwd hashes a password.
func HashAndValidatePwd(password string, minimumLength uint) (string, error) {
if uint(len(password)) < minimumLength {
return "", fbErrors.ErrShortPassword
}
return HashPwd(password)
}
// HashPwd hashes a password.
func HashPwd(password string) (string, error) {
@ -22,8 +30,8 @@ func CheckPwd(password, hash string) bool {
return err == nil
}
func RandomPwd() (string, error) {
randomPasswordBytes := make([]byte, randomPasswordBytesCount)
func RandomPwd(passwordLength uint) (string, error) {
randomPasswordBytes := make([]byte, passwordLength)
var _, err = rand.Read(randomPasswordBytes)
if err != nil {
return "", err