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) | | ![](./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 ## Install
For information on how to install File Browser, please check [docs/installation.md](./docs/installation.md). 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 { if u == nil {
pass, err := users.HashPwd(a.Cred.Password) pass, err := users.HashAndValidatePwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -186,7 +186,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
// update the password when it doesn't match the current // update the password when it doesn't match the current
if p { if p {
pass, err := users.HashPwd(a.Cred.Password) pass, err := users.HashAndValidatePwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

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

View File

@ -32,6 +32,7 @@ func addConfigFlags(flags *pflag.FlagSet) {
addUserFlags(flags) addUserFlags(flags)
flags.BoolP("signup", "s", false, "allow users to signup") flags.BoolP("signup", "s", false, "allow users to signup")
flags.Bool("create-user-dir", false, "generate user's home directory automatically") 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("shell", "", "shell command to which other commands should be appended")
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type") 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, "Sign up:\t%t\n", set.Signup)
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir) 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, "Auth method:\t%s\n", set.AuthMethod)
fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " ")) fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
fmt.Fprintln(w, "\nBranding:") fmt.Fprintln(w, "\nBranding:")

View File

@ -32,6 +32,7 @@ override the options.`,
Key: generateKey(), Key: generateKey(),
Signup: mustGetBool(flags, "signup"), Signup: mustGetBool(flags, "signup"),
CreateUserDir: mustGetBool(flags, "create-user-dir"), CreateUserDir: mustGetBool(flags, "create-user-dir"),
MinimumPasswordLength: mustGetUint(flags, "minimum-password-length"),
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")), Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
AuthMethod: authMethod, AuthMethod: authMethod,
Defaults: defaults, Defaults: defaults,

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@ var (
ErrExist = errors.New("the resource already exists") ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist") ErrNotExist = errors.New("the resource does not exist")
ErrEmptyPassword = errors.New("password is empty") ErrEmptyPassword = errors.New("password is empty")
ErrShortPassword = errors.New("password is too short")
ErrEmptyUsername = errors.New("username is empty") ErrEmptyUsername = errors.New("username is empty")
ErrEmptyRequest = errors.New("empty request") ErrEmptyRequest = errors.New("empty request")
ErrScopeIsRelative = errors.New("scope is a relative path") 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}.", "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!", "commandsUpdated": "Commands updated!",
"createUserDir": "Auto create user home dir while adding new user", "createUserDir": "Auto create user home dir while adding new user",
"minimumPasswordLength": "Minimum password length",
"tusUploads": "Chunked Uploads", "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.", "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.", "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 { interface ISettings {
signup: boolean; signup: boolean;
createUserDir: boolean; createUserDir: boolean;
minimumPasswordLength: number;
userHomeBasePath: string; userHomeBasePath: string;
defaults: SettingsDefaults; defaults: SettingsDefaults;
rules: any[]; rules: any[];

View File

@ -18,14 +18,26 @@
{{ t("settings.createUserDir") }} {{ t("settings.createUserDir") }}
</p> </p>
<div> <p>
<p class="small">{{ t("settings.userHomeBasePath") }}</p> <label class="small">{{ t("settings.userHomeBasePath") }}</label>
<input <input
class="input input--block" class="input input--block"
type="text" type="text"
v-model="settings.userHomeBasePath" 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> <h3>{{ t("settings.rules") }}</h3>
<p class="small">{{ t("settings.globalRules") }}</p> <p class="small">{{ t("settings.globalRules") }}</p>
@ -229,17 +241,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
import { settings as api } from "@/api"; import { settings as api } from "@/api";
import { enableExec } from "@/utils/constants"; import { StatusError } from "@/api/utils";
import UserForm from "@/components/settings/UserForm.vue";
import Rules from "@/components/settings/Rules.vue"; import Rules from "@/components/settings/Rules.vue";
import Themes from "@/components/settings/Themes.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 Errors from "@/views/Errors.vue";
import { computed, inject, onBeforeUnmount, onMounted, ref } from "vue"; import { computed, inject, onBeforeUnmount, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { StatusError } from "@/api/utils";
import { getTheme, setTheme } from "@/utils/theme";
const error = ref<StatusError | null>(null); const error = ref<StatusError | null>(null);
const originalSettings = ref<ISettings | 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) d.settings.Defaults.Apply(user)
pwd, err := users.HashPwd(info.Password) pwd, err := users.HashAndValidatePwd(info.Password, d.settings.MinimumPasswordLength)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }

View File

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

View File

@ -11,6 +11,7 @@ import (
type settingsData struct { type settingsData struct {
Signup bool `json:"signup"` Signup bool `json:"signup"`
CreateUserDir bool `json:"createUserDir"` CreateUserDir bool `json:"createUserDir"`
MinimumPasswordLength uint `json:"minimumPasswordLength"`
UserHomeBasePath string `json:"userHomeBasePath"` UserHomeBasePath string `json:"userHomeBasePath"`
Defaults settings.UserDefaults `json:"defaults"` Defaults settings.UserDefaults `json:"defaults"`
Rules []rules.Rule `json:"rules"` Rules []rules.Rule `json:"rules"`
@ -24,6 +25,7 @@ var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request,
data := &settingsData{ data := &settingsData{
Signup: d.settings.Signup, Signup: d.settings.Signup,
CreateUserDir: d.settings.CreateUserDir, CreateUserDir: d.settings.CreateUserDir,
MinimumPasswordLength: d.settings.MinimumPasswordLength,
UserHomeBasePath: d.settings.UserHomeBasePath, UserHomeBasePath: d.settings.UserHomeBasePath,
Defaults: d.settings.Defaults, Defaults: d.settings.Defaults,
Rules: d.settings.Rules, Rules: d.settings.Rules,
@ -45,6 +47,7 @@ var settingsPutHandler = withAdmin(func(_ http.ResponseWriter, r *http.Request,
d.settings.Signup = req.Signup d.settings.Signup = req.Signup
d.settings.CreateUserDir = req.CreateUserDir d.settings.CreateUserDir = req.CreateUserDir
d.settings.MinimumPasswordLength = req.MinimumPasswordLength
d.settings.UserHomeBasePath = req.UserHomeBasePath d.settings.UserHomeBasePath = req.UserHomeBasePath
d.settings.Defaults = req.Defaults d.settings.Defaults = req.Defaults
d.settings.Rules = req.Rules 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 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 { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -163,7 +167,7 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
} }
if req.Data.Password != "" { 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 { } else {
var suser *users.User var suser *users.User
suser, err = d.store.Users.Get(d.server.Root, d.raw.(uint)) 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 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 { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }

View File

@ -10,6 +10,7 @@ import (
) )
const DefaultUsersHomeBasePath = "/users" const DefaultUsersHomeBasePath = "/users"
const DefaultMinimumPasswordLength = 12
// AuthMethod describes an authentication method. // AuthMethod describes an authentication method.
type AuthMethod string type AuthMethod string
@ -27,6 +28,7 @@ type Settings struct {
Commands map[string][]string `json:"commands"` Commands map[string][]string `json:"commands"`
Shell []string `json:"shell"` Shell []string `json:"shell"`
Rules []rules.Rule `json:"rules"` Rules []rules.Rule `json:"rules"`
MinimumPasswordLength uint `json:"minimumPasswordLength"`
} }
// GetRules implements rules.Provider. // GetRules implements rules.Provider.

View File

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

View File

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