mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
fix(web): refuse oversized login usernames before recording them
A failed login keeps its username in the in-memory log buffer, the limiter table and the notifier queue, and the route accepted any body the global cap allowed. The login route now takes at most 16 KiB and answers a username over 256 bytes with the generic failure before any of those see it; the user setters refuse such a name too, so no stored username can be one the login route turns away.
This commit is contained in:
@@ -2,6 +2,7 @@ package panel
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -128,6 +129,9 @@ func (s *UserService) BumpLoginEpoch() error {
|
||||
}
|
||||
|
||||
func (s *UserService) UpdateUser(id int, username string, password string) error {
|
||||
if len(username) > MaxUsernameLength {
|
||||
return fmt.Errorf("username can not be longer than %d bytes", MaxUsernameLength)
|
||||
}
|
||||
db := database.GetDB()
|
||||
hashedPassword, err := crypto.HashPasswordAsBcrypt(password)
|
||||
if err != nil {
|
||||
@@ -157,6 +161,8 @@ func (s *UserService) UpdateUser(id int, username string, password string) error
|
||||
func (s *UserService) UpdateFirstUser(username string, password string) error {
|
||||
if username == "" {
|
||||
return errors.New("username can not be empty")
|
||||
} else if len(username) > MaxUsernameLength {
|
||||
return fmt.Errorf("username can not be longer than %d bytes", MaxUsernameLength)
|
||||
} else if password == "" {
|
||||
return errors.New("password can not be empty")
|
||||
}
|
||||
@@ -181,3 +187,7 @@ func (s *UserService) UpdateFirstUser(username string, password string) error {
|
||||
user.LoginEpoch++
|
||||
return db.Save(user).Error
|
||||
}
|
||||
|
||||
// MaxUsernameLength bounds a panel username: the login route refuses longer names before
|
||||
// logging or recording them, so no stored name may exceed it.
|
||||
const MaxUsernameLength = 256
|
||||
|
||||
@@ -2,6 +2,7 @@ package panel
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
@@ -48,3 +49,28 @@ func TestHasDefaultCredential(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The login route refuses names over MaxUsernameLength unread, so storing one would lock
|
||||
// the admin out of their own panel.
|
||||
func TestUserSettersRefuseNamesTheLoginRouteRefuses(t *testing.T) {
|
||||
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
|
||||
svc := &UserService{}
|
||||
if err := svc.UpdateFirstUser("admin", "s3cret-Pass"); err != nil {
|
||||
t.Fatalf("UpdateFirstUser: %v", err)
|
||||
}
|
||||
var user model.User
|
||||
if err := database.GetDB().First(&user).Error; err != nil {
|
||||
t.Fatalf("load user: %v", err)
|
||||
}
|
||||
long := strings.Repeat("a", MaxUsernameLength+1)
|
||||
if err := svc.UpdateFirstUser(long, "s3cret-Pass"); err == nil {
|
||||
t.Fatal("UpdateFirstUser stored a name the login route refuses")
|
||||
}
|
||||
if err := svc.UpdateUser(user.Id, long, "s3cret-Pass"); err == nil {
|
||||
t.Fatal("UpdateUser stored a name the login route refuses")
|
||||
}
|
||||
var stored model.User
|
||||
if err := database.GetDB().First(&stored, user.Id).Error; err != nil || stored.Username != "admin" {
|
||||
t.Fatalf("stored username = %q (%v), want it unchanged", stored.Username, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user