filebrowser/storage/bolt/users.go
Henrique Dias 4b8a8d72ce
fix(storage): reject case-folded home directory collisions
GetByScope compared scopes with a case-sensitive exact match, so on a
case-insensitive filesystem two accounts whose scopes differ only in case
(e.g. /users/CaseVictim and /users/casevictim) were treated as distinct even
though both resolve to the same physical home directory. The second account
could then read, overwrite and delete the first account's files.

Match the scope case-insensitively so the collision check performed during
signup, proxy and hook provisioning catches these aliases and rejects the
duplicate.

Refs GHSA-576v-w77m-gr84
2026-07-25 08:16:18 +02:00

141 lines
2.8 KiB
Go

package bolt
import (
"errors"
"fmt"
"reflect"
"regexp"
"github.com/asdine/storm/v3"
"github.com/asdine/storm/v3/q"
bolt "go.etcd.io/bbolt"
fberrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/users"
)
type usersBackend struct {
db *storm.DB
}
func (st usersBackend) GetBy(i interface{}) (user *users.User, err error) {
user = &users.User{}
var arg string
switch i.(type) {
case uint:
arg = "ID"
case string:
arg = "Username"
default:
return nil, fberrors.ErrInvalidDataType
}
err = st.db.One(arg, i, user)
if err != nil {
if errors.Is(err, storm.ErrNotFound) {
return nil, fberrors.ErrNotExist
}
return nil, err
}
return
}
func (st usersBackend) GetByScope(scope string) (*users.User, error) {
user := &users.User{}
// Match case-insensitively: on a case-insensitive filesystem two scopes
// that differ only in case (e.g. /users/Alice and /users/alice) resolve to
// the same home directory, so they must be treated as a collision.
pattern := "(?i)^" + regexp.QuoteMeta(scope) + "$"
err := st.db.Select(q.Re("Scope", pattern)).First(user)
if err != nil {
if errors.Is(err, storm.ErrNotFound) {
return nil, fberrors.ErrNotExist
}
return nil, err
}
return user, nil
}
func (st usersBackend) Gets() ([]*users.User, error) {
var allUsers []*users.User
err := st.db.All(&allUsers)
if errors.Is(err, storm.ErrNotFound) {
return nil, fberrors.ErrNotExist
}
if err != nil {
return allUsers, err
}
return allUsers, err
}
func (st usersBackend) Update(user *users.User, fields ...string) error {
if len(fields) == 0 {
return st.Save(user)
}
for _, field := range fields {
userField := reflect.ValueOf(user).Elem().FieldByName(field)
if !userField.IsValid() {
return fmt.Errorf("invalid field: %s", field)
}
val := userField.Interface()
if err := st.db.UpdateField(user, field, val); err != nil {
return err
}
}
return nil
}
func (st usersBackend) Save(user *users.User) error {
err := st.db.Save(user)
if errors.Is(err, storm.ErrAlreadyExists) {
return fberrors.ErrExist
}
return err
}
func (st usersBackend) DeleteByID(id uint) error {
return st.db.DeleteStruct(&users.User{ID: id})
}
func (st usersBackend) DeleteByUsername(username string) error {
user, err := st.GetBy(username)
if err != nil {
return err
}
return st.db.DeleteStruct(user)
}
func (st usersBackend) CountAdmins() (int, error) {
count := 0
err := st.db.Bolt.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(reflect.TypeOf(users.User{}).Name()))
if bucket == nil {
return nil
}
c := bucket.Cursor()
for _, v := c.First(); v != nil; _, v = c.Next() {
var u users.User
if err := st.db.Codec().Unmarshal(v, &u); err != nil {
return err
}
if u.Perm.Admin {
count++
}
}
return nil
})
return count, err
}