fix: add configurable minimum password length

pull/5225/head
Henrique Dias 2025-06-28 09:41:11 +02:00
parent 089255997a
commit 2f3517e627
No known key found for this signature in database
19 changed files with 113 additions and 67 deletions

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,13 @@ 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)
pwd, err := users.RandomPwd(setting.MinimumPasswordLength + 10)
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", 12, "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

@ -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

@ -4,11 +4,18 @@ import (
"crypto/rand"
"encoding/base64"
"github.com/filebrowser/filebrowser/v2/errors"
"golang.org/x/crypto/bcrypt"
)
// 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 len(password) < int(minimumLength) {
return "", errors.ErrShortPassword
}
return HashPwd(password)
}
// HashPwd hashes a password.
func HashPwd(password string) (string, error) {
@ -22,8 +29,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