fix(security): add login count validation for webdav (#693)

This commit is contained in:
itsHenry
2025-07-12 18:03:41 +09:00
committed by GitHub
parent 283f3723d1
commit e5fbe72581
4 changed files with 39 additions and 21 deletions

View File

@ -9,6 +9,7 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/pkg/utils/random"
"github.com/OpenListTeam/go-cache"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/pkg/errors"
)
@ -21,6 +22,13 @@ const (
const StaticHashSalt = "https://github.com/alist-org/alist"
var LoginCache = cache.NewMemCache[int]()
var (
DefaultLockDuration = time.Minute * 5
DefaultMaxAuthRetries = 5
)
type User struct {
ID uint `json:"id" gorm:"primaryKey"` // unique key
Username string `json:"username" gorm:"unique" binding:"required"` // username

View File

@ -4,22 +4,14 @@ import (
"bytes"
"encoding/base64"
"image/png"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/server/common"
"github.com/OpenListTeam/go-cache"
"github.com/gin-gonic/gin"
"github.com/pquerna/otp/totp"
)
var loginCache = cache.NewMemCache[int]()
var (
defaultDuration = time.Minute * 5
defaultTimes = 5
)
type LoginReq struct {
Username string `json:"username" binding:"required"`
Password string `json:"password"`
@ -50,30 +42,30 @@ func LoginHash(c *gin.Context) {
func loginHash(c *gin.Context, req *LoginReq) {
// check count of login
ip := c.ClientIP()
count, ok := loginCache.Get(ip)
if ok && count >= defaultTimes {
count, ok := model.LoginCache.Get(ip)
if ok && count >= model.DefaultMaxAuthRetries {
common.ErrorStrResp(c, "Too many unsuccessful sign-in attempts have been made using an incorrect username or password, Try again later.", 429)
loginCache.Expire(ip, defaultDuration)
model.LoginCache.Expire(ip, model.DefaultLockDuration)
return
}
// check username
user, err := op.GetUserByName(req.Username)
if err != nil {
common.ErrorResp(c, err, 400)
loginCache.Set(ip, count+1)
model.LoginCache.Set(ip, count+1)
return
}
// validate password hash
if err := user.ValidatePwdStaticHash(req.Password); err != nil {
common.ErrorResp(c, err, 400)
loginCache.Set(ip, count+1)
model.LoginCache.Set(ip, count+1)
return
}
// check 2FA
if user.OtpSecret != "" {
if !totp.Validate(req.OtpCode, user.OtpSecret) {
common.ErrorStrResp(c, "Invalid 2FA code", 402)
loginCache.Set(ip, count+1)
model.LoginCache.Set(ip, count+1)
return
}
}
@ -84,7 +76,7 @@ func loginHash(c *gin.Context, req *LoginReq) {
return
}
common.SuccessResp(c, gin.H{"token": token})
loginCache.Del(ip)
model.LoginCache.Del(ip)
}
type UserResp struct {

View File

@ -36,10 +36,10 @@ func loginLdap(c *gin.Context, req *LoginReq) {
// check count of login
ip := c.ClientIP()
count, ok := loginCache.Get(ip)
if ok && count >= defaultTimes {
count, ok := model.LoginCache.Get(ip)
if ok && count >= model.DefaultMaxAuthRetries {
common.ErrorStrResp(c, "Too many unsuccessful sign-in attempts have been made using an incorrect username or password, Try again later.", 429)
loginCache.Expire(ip, defaultDuration)
model.LoginCache.Expire(ip, model.DefaultLockDuration)
return
}
@ -94,7 +94,7 @@ func loginLdap(c *gin.Context, req *LoginReq) {
if err != nil {
utils.Log.Errorf("Failed to auth. %v", err)
common.ErrorResp(c, err, 400)
loginCache.Set(ip, count+1)
model.LoginCache.Set(ip, count+1)
return
} else {
utils.Log.Infof("Auth successful username:%s", req.Username)
@ -106,7 +106,7 @@ func loginLdap(c *gin.Context, req *LoginReq) {
user, err = ladpRegister(req.Username)
if err != nil {
common.ErrorResp(c, err, 400)
loginCache.Set(ip, count+1)
model.LoginCache.Set(ip, count+1)
return
}
}
@ -118,7 +118,7 @@ func loginLdap(c *gin.Context, req *LoginReq) {
return
}
common.SuccessResp(c, gin.H{"token": token})
loginCache.Del(ip)
model.LoginCache.Del(ip)
}
func ladpRegister(username string) (*model.User, error) {

View File

@ -6,6 +6,7 @@ import (
"path"
"strings"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/server/middlewares"
@ -47,7 +48,21 @@ func ServeWebDAV(c *gin.Context) {
}
func WebDAVAuth(c *gin.Context) {
// check count of login
ip := c.ClientIP()
guest, _ := op.GetGuest()
count, cok := model.LoginCache.Get(ip)
if cok && count >= model.DefaultMaxAuthRetries {
if c.Request.Method == "OPTIONS" {
c.Set("user", guest)
c.Next()
return
}
c.Status(http.StatusTooManyRequests)
c.Abort()
model.LoginCache.Expire(ip, model.DefaultLockDuration)
return
}
username, password, ok := c.Request.BasicAuth()
if !ok {
bt := c.GetHeader("Authorization")
@ -85,10 +100,13 @@ func WebDAVAuth(c *gin.Context) {
c.Next()
return
}
model.LoginCache.Set(ip, count+1)
c.Status(http.StatusUnauthorized)
c.Abort()
return
}
// at least auth is successful till here
model.LoginCache.Del(ip)
if user.Disabled || !user.CanWebdavRead() {
if c.Request.Method == "OPTIONS" {
c.Set("user", guest)