From 897dcdef40eec7a71b7efbcb9f4c7ce8387ab486 Mon Sep 17 00:00:00 2001 From: Patrick Zhao Date: Tue, 3 Mar 2026 14:12:24 +0800 Subject: [PATCH 1/4] fix captcha security issue Signed-off-by: Patrick Zhao --- .../user/core/handler/login/local.go | 3 +- .../user/core/service/login/local.go | 104 +++++++++++++----- 2 files changed, 77 insertions(+), 30 deletions(-) diff --git a/pkg/microservice/user/core/handler/login/local.go b/pkg/microservice/user/core/handler/login/local.go index ef41348c91..5fa1eea9f7 100644 --- a/pkg/microservice/user/core/handler/login/local.go +++ b/pkg/microservice/user/core/handler/login/local.go @@ -49,7 +49,8 @@ func GetCaptcha(c *gin.Context) { ctx := internalhandler.NewContext(c) defer func() { internalhandler.JSONResponse(c, ctx) }() - id, picBase64, err := login.GetCaptcha(ctx.Logger) + account := c.Query("account") + id, picBase64, err := login.GetCaptcha(account, ctx.Logger) if err != nil { ctx.RespErr = err return diff --git a/pkg/microservice/user/core/service/login/local.go b/pkg/microservice/user/core/service/login/local.go index 365c743d62..a96abb9dc3 100644 --- a/pkg/microservice/user/core/service/login/local.go +++ b/pkg/microservice/user/core/service/login/local.go @@ -18,6 +18,7 @@ package login import ( "fmt" + "strings" "time" "github.com/golang-jwt/jwt" @@ -97,9 +98,64 @@ func CheckSignature(lastLoginTime int64, logger *zap.SugaredLogger) error { } var ( - loginCache = cache.New(time.Hour, time.Second*10) + loginCache = cache.New(time.Hour, time.Second*10) + captchaAccountCache = cache.New(base64Captcha.Expiration, time.Second*10) ) +const ( + loginFailedLimit = 5 +) + +func getLoginFailedCount(uid string, logger *zap.SugaredLogger) int { + failedCountInterface, found := loginCache.Get(uid) + if !found { + return 0 + } + failedCount, ok := failedCountInterface.(int) + if !ok { + logger.Warnf("unexpected login failed count type for UID: [%s], reset cache entry", uid) + loginCache.Delete(uid) + return 0 + } + return failedCount +} + +func incrementLoginFailedCount(uid string, logger *zap.SugaredLogger) int { + if err := loginCache.Add(uid, 1, time.Hour); err == nil { + return 1 + } + + failedCount, err := loginCache.IncrementInt(uid, 1) + if err != nil { + logger.Errorf("failed to increment login failed count for UID: [%s], error: %s", uid, err) + loginCache.Set(uid, 1, time.Hour) + return 1 + } + return failedCount +} + +func verifyCaptchaForAccount(captchaID, captchaAnswer, account string) error { + accountInfo, found := captchaAccountCache.Get(captchaID) + // Binding is single-use once submitted. + captchaAccountCache.Delete(captchaID) + if !found { + return fmt.Errorf("captcha is invalid") + } + boundAccount, ok := accountInfo.(string) + if !ok { + _ = store.Get(captchaID, true) + return fmt.Errorf("captcha is invalid") + } + if boundAccount != account { + _ = store.Get(captchaID, true) + return fmt.Errorf("captcha is invalid") + } + if passed := store.Verify(captchaID, captchaAnswer, true); !passed { + return fmt.Errorf("captcha is wrong") + } + return nil +} + func LocalLogin(args *LoginArgs, logger *zap.SugaredLogger) (*User, int, error) { user, err := orm.GetUser(args.Account, config.SystemIdentityType, repository.DB) if err != nil { @@ -119,39 +175,22 @@ func LocalLogin(args *LoginArgs, logger *zap.SugaredLogger) (*User, int, error) return nil, 0, fmt.Errorf("user login not exist") } - failedCountInterface, failedCountfound := loginCache.Get(user.UID) - - if failedCountfound { - if failedCountInterface.(int) >= 5 { - // first check if a captcha answer is provided - if args.CaptchaAnswer == "" || args.CaptchaID == "" { - return nil, 5, fmt.Errorf("captcha is required") - } - - // captcha validation - if passed := store.Verify(args.CaptchaID, args.CaptchaAnswer, false); !passed { - return nil, 5, fmt.Errorf("captcha is wrong") - } + failedCount := getLoginFailedCount(user.UID, logger) + if failedCount >= loginFailedLimit { + // first check if a captcha answer is provided + if args.CaptchaAnswer == "" || args.CaptchaID == "" { + return nil, failedCount, fmt.Errorf("captcha is required") + } + if err := verifyCaptchaForAccount(args.CaptchaID, args.CaptchaAnswer, args.Account); err != nil { + return nil, failedCount, err } } password := []byte(args.Password) err = bcrypt.CompareHashAndPassword([]byte(userLogin.Password), password) if err == bcrypt.ErrMismatchedHashAndPassword { - - if !failedCountfound { - loginCache.Set(user.UID, 1, time.Hour) - } else { - err := loginCache.Increment(user.UID, 1) - if err != nil { - logger.Errorf("failed to do login cache increment for UID: [%s], error: %s", user.UID, err) - } - } - failedCount, ok := failedCountInterface.(int) - if !ok { - failedCount = 0 - } - return nil, failedCount + 1, fmt.Errorf("password is wrong") + failedCount = incrementLoginFailedCount(user.UID, logger) + return nil, failedCount, fmt.Errorf("password is wrong") } if err != nil { logger.Errorf("LocalLogin user:%s check password error, error msg:%s", args.Account, err) @@ -260,7 +299,12 @@ func LocalLogout(userID string, logger *zap.SugaredLogger) (bool, string, error) var store = base64Captcha.DefaultMemStore -func GetCaptcha(logger *zap.SugaredLogger) (string, string, error) { +func GetCaptcha(account string, logger *zap.SugaredLogger) (string, string, error) { + account = strings.TrimSpace(account) + if account == "" { + return "", "", fmt.Errorf("account is required") + } + driver := base64Captcha.DefaultDriverDigit c := base64Captcha.NewCaptcha(driver, store) @@ -269,5 +313,7 @@ func GetCaptcha(logger *zap.SugaredLogger) (string, string, error) { logger.Errorf("failed to generate captcha, error: %s", err) return "", "", fmt.Errorf("captcha generate error") } + + captchaAccountCache.Set(id, account, base64Captcha.Expiration) return id, b64s, nil } From 5e09112c30b78f50ed2396b1af477eee5e85edd0 Mon Sep 17 00:00:00 2001 From: Patrick Zhao Date: Tue, 3 Mar 2026 15:11:39 +0800 Subject: [PATCH 2/4] encrypt password in local login Signed-off-by: Patrick Zhao --- .../user/core/service/login/local.go | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/pkg/microservice/user/core/service/login/local.go b/pkg/microservice/user/core/service/login/local.go index a96abb9dc3..f28d0d6ee8 100644 --- a/pkg/microservice/user/core/service/login/local.go +++ b/pkg/microservice/user/core/service/login/local.go @@ -36,6 +36,7 @@ import ( "github.com/koderover/zadig/v2/pkg/shared/client/aslan" "github.com/koderover/zadig/v2/pkg/shared/client/plutusvendor" zadigCache "github.com/koderover/zadig/v2/pkg/tool/cache" + "github.com/koderover/zadig/v2/pkg/tool/crypto" ) type LoginArgs struct { @@ -156,22 +157,52 @@ func verifyCaptchaForAccount(captchaID, captchaAnswer, account string) error { return nil } +func decryptLoginPassword(password string, logger *zap.SugaredLogger) (string, error) { + secretKey := strings.TrimSpace(configbase.SecretKey()) + if secretKey == "" { + logger.Errorf("failed to decrypt password, SECRET_KEY is empty") + return "", fmt.Errorf("login password decrypt is not configured") + } + + plainText, err := crypto.AesDecrypt(strings.TrimSpace(password), secretKey) + if err != nil { + logger.Errorf("failed to decrypt password, error: %s", err) + return "", fmt.Errorf("invalid password") + } + if len(plainText) == 0 { + return "", fmt.Errorf("invalid password") + } + return plainText, nil +} + func LocalLogin(args *LoginArgs, logger *zap.SugaredLogger) (*User, int, error) { - user, err := orm.GetUser(args.Account, config.SystemIdentityType, repository.DB) + account := strings.TrimSpace(args.Account) + if account == "" { + return nil, 0, fmt.Errorf("account is required") + } + if strings.TrimSpace(args.Password) == "" { + return nil, 0, fmt.Errorf("password is required") + } + passwordText, err := decryptLoginPassword(args.Password, logger) + if err != nil { + return nil, 0, err + } + + user, err := orm.GetUser(account, config.SystemIdentityType, repository.DB) if err != nil { - logger.Errorf("InternalLogin get user account:%s error", args.Account) + logger.Errorf("InternalLogin get user account:%s error", account) return nil, 0, err } if user == nil { return nil, 0, fmt.Errorf("user not exist") } - userLogin, err := orm.GetUserLogin(user.UID, args.Account, config.AccountLoginType, repository.DB) + userLogin, err := orm.GetUserLogin(user.UID, account, config.AccountLoginType, repository.DB) if err != nil { - logger.Errorf("LocalLogin get user:%s user login not exist, error msg:%s", args.Account, err.Error()) + logger.Errorf("LocalLogin get user:%s user login not exist, error msg:%s", account, err.Error()) return nil, 0, err } if userLogin == nil { - logger.Errorf("InternalLogin user:%s user login not exist", args.Account) + logger.Errorf("InternalLogin user:%s user login not exist", account) return nil, 0, fmt.Errorf("user login not exist") } @@ -181,19 +212,19 @@ func LocalLogin(args *LoginArgs, logger *zap.SugaredLogger) (*User, int, error) if args.CaptchaAnswer == "" || args.CaptchaID == "" { return nil, failedCount, fmt.Errorf("captcha is required") } - if err := verifyCaptchaForAccount(args.CaptchaID, args.CaptchaAnswer, args.Account); err != nil { + if err := verifyCaptchaForAccount(args.CaptchaID, args.CaptchaAnswer, account); err != nil { return nil, failedCount, err } } - password := []byte(args.Password) + password := []byte(passwordText) err = bcrypt.CompareHashAndPassword([]byte(userLogin.Password), password) if err == bcrypt.ErrMismatchedHashAndPassword { failedCount = incrementLoginFailedCount(user.UID, logger) return nil, failedCount, fmt.Errorf("password is wrong") } if err != nil { - logger.Errorf("LocalLogin user:%s check password error, error msg:%s", args.Account, err) + logger.Errorf("LocalLogin user:%s check password error, error msg:%s", account, err) return nil, 0, fmt.Errorf("check password error, error msg:%s", err) } @@ -205,7 +236,7 @@ func LocalLogin(args *LoginArgs, logger *zap.SugaredLogger) (*User, int, error) userLogin.LastLoginTime = time.Now().Unix() err = orm.UpdateUserLogin(userLogin.UID, userLogin, repository.DB) if err != nil { - logger.Errorf("LocalLogin user:%s update user login password error, error msg:%s", args.Account, err.Error()) + logger.Errorf("LocalLogin user:%s update user login password error, error msg:%s", account, err.Error()) return nil, 0, err } @@ -230,13 +261,13 @@ func LocalLogin(args *LoginArgs, logger *zap.SugaredLogger) (*User, int, error) }, }) if err != nil { - logger.Errorf("LocalLogin user:%s create token error, error msg:%s", args.Account, err.Error()) + logger.Errorf("LocalLogin user:%s create token error, error msg:%s", account, err.Error()) return nil, 0, err } groupIDList, err := common.GetUserGroupByUID(user.UID) if err != nil { - logger.Errorf("LocalLogin get user:%s group error, error msg:%s", args.Account, err.Error()) + logger.Errorf("LocalLogin get user:%s group error, error msg:%s", account, err.Error()) return nil, 0, err } allUserGroupID, err := common.GetAllUserGroup() From f0513186fbdf5e04e13f83f8e5663d617b4a0cc9 Mon Sep 17 00:00:00 2001 From: Patrick Zhao Date: Tue, 3 Mar 2026 15:24:28 +0800 Subject: [PATCH 3/4] harden login errors Signed-off-by: Patrick Zhao --- .../user/core/service/login/local.go | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/pkg/microservice/user/core/service/login/local.go b/pkg/microservice/user/core/service/login/local.go index f28d0d6ee8..5ab470c2f4 100644 --- a/pkg/microservice/user/core/service/login/local.go +++ b/pkg/microservice/user/core/service/login/local.go @@ -135,6 +135,10 @@ func incrementLoginFailedCount(uid string, logger *zap.SugaredLogger) int { return failedCount } +func loginFailedCacheKey(account string) string { + return fmt.Sprintf("account:%s", account) +} + func verifyCaptchaForAccount(captchaID, captchaAnswer, account string) error { accountInfo, found := captchaAccountCache.Get(captchaID) // Binding is single-use once submitted. @@ -180,6 +184,7 @@ func LocalLogin(args *LoginArgs, logger *zap.SugaredLogger) (*User, int, error) if account == "" { return nil, 0, fmt.Errorf("account is required") } + loginFailedKey := loginFailedCacheKey(account) if strings.TrimSpace(args.Password) == "" { return nil, 0, fmt.Errorf("password is required") } @@ -188,13 +193,25 @@ func LocalLogin(args *LoginArgs, logger *zap.SugaredLogger) (*User, int, error) return nil, 0, err } + failedCount := getLoginFailedCount(loginFailedKey, logger) + if failedCount >= loginFailedLimit { + // first check if a captcha answer is provided + if args.CaptchaAnswer == "" || args.CaptchaID == "" { + return nil, failedCount, fmt.Errorf("captcha is required") + } + if err := verifyCaptchaForAccount(args.CaptchaID, args.CaptchaAnswer, account); err != nil { + return nil, failedCount, err + } + } + user, err := orm.GetUser(account, config.SystemIdentityType, repository.DB) if err != nil { logger.Errorf("InternalLogin get user account:%s error", account) return nil, 0, err } if user == nil { - return nil, 0, fmt.Errorf("user not exist") + failedCount = incrementLoginFailedCount(loginFailedKey, logger) + return nil, failedCount, fmt.Errorf("invalid username or password") } userLogin, err := orm.GetUserLogin(user.UID, account, config.AccountLoginType, repository.DB) if err != nil { @@ -203,25 +220,15 @@ func LocalLogin(args *LoginArgs, logger *zap.SugaredLogger) (*User, int, error) } if userLogin == nil { logger.Errorf("InternalLogin user:%s user login not exist", account) - return nil, 0, fmt.Errorf("user login not exist") - } - - failedCount := getLoginFailedCount(user.UID, logger) - if failedCount >= loginFailedLimit { - // first check if a captcha answer is provided - if args.CaptchaAnswer == "" || args.CaptchaID == "" { - return nil, failedCount, fmt.Errorf("captcha is required") - } - if err := verifyCaptchaForAccount(args.CaptchaID, args.CaptchaAnswer, account); err != nil { - return nil, failedCount, err - } + failedCount = incrementLoginFailedCount(loginFailedKey, logger) + return nil, failedCount, fmt.Errorf("invalid username or password") } password := []byte(passwordText) err = bcrypt.CompareHashAndPassword([]byte(userLogin.Password), password) if err == bcrypt.ErrMismatchedHashAndPassword { - failedCount = incrementLoginFailedCount(user.UID, logger) - return nil, failedCount, fmt.Errorf("password is wrong") + failedCount = incrementLoginFailedCount(loginFailedKey, logger) + return nil, failedCount, fmt.Errorf("invalid username or password") } if err != nil { logger.Errorf("LocalLogin user:%s check password error, error msg:%s", account, err) @@ -281,6 +288,7 @@ func LocalLogin(args *LoginArgs, logger *zap.SugaredLogger) (*User, int, error) if err != nil { logger.Errorf("failed to write token into cache, error: %s\n warn: this will cause login failure", err) } + loginCache.Delete(loginFailedKey) return &User{ Uid: user.UID, From 677f972ee285bd16eaa9e3b87ed167f0e1139a36 Mon Sep 17 00:00:00 2001 From: Patrick Zhao Date: Wed, 4 Mar 2026 15:20:14 +0800 Subject: [PATCH 4/4] change login password decrypt method Signed-off-by: Patrick Zhao --- .../user/core/service/login/local.go | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/pkg/microservice/user/core/service/login/local.go b/pkg/microservice/user/core/service/login/local.go index 5ab470c2f4..dad216aaef 100644 --- a/pkg/microservice/user/core/service/login/local.go +++ b/pkg/microservice/user/core/service/login/local.go @@ -32,6 +32,7 @@ import ( "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository" "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/orm" "github.com/koderover/zadig/v2/pkg/microservice/user/core/service/common" + aslanutil "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/util" "github.com/koderover/zadig/v2/pkg/setting" "github.com/koderover/zadig/v2/pkg/shared/client/aslan" "github.com/koderover/zadig/v2/pkg/shared/client/plutusvendor" @@ -41,6 +42,7 @@ import ( type LoginArgs struct { Account string `json:"account"` + EncryptedKey string `json:"encrypted_key"` Password string `json:"password"` CaptchaID string `json:"captcha_id"` CaptchaAnswer string `json:"captcha_answer"` @@ -161,22 +163,32 @@ func verifyCaptchaForAccount(captchaID, captchaAnswer, account string) error { return nil } -func decryptLoginPassword(password string, logger *zap.SugaredLogger) (string, error) { - secretKey := strings.TrimSpace(configbase.SecretKey()) - if secretKey == "" { - logger.Errorf("failed to decrypt password, SECRET_KEY is empty") - return "", fmt.Errorf("login password decrypt is not configured") +func decryptLoginPassword(encryptedKey, encryptedPassword string, logger *zap.SugaredLogger) (string, error) { + if encryptedKey == "" { + return "", fmt.Errorf("encrypted_key is required") + } + if encryptedPassword == "" { + return "", fmt.Errorf("password is required") } - plainText, err := crypto.AesDecrypt(strings.TrimSpace(password), secretKey) + aesKeyResp, err := aslanutil.GetAesKeyFromEncryptedKey(encryptedKey, logger) if err != nil { - logger.Errorf("failed to decrypt password, error: %s", err) - return "", fmt.Errorf("invalid password") + logger.Errorf("failed to decrypt aes key by GetAesKeyFromEncryptedKey, error: %s", err) + return "", fmt.Errorf("invalid encrypted_key") + } + if aesKeyResp == nil || aesKeyResp.PlainText == "" { + return "", fmt.Errorf("invalid encrypted_key") + } + + password, err := crypto.AesDecrypt(encryptedPassword, aesKeyResp.PlainText) + if err != nil { + logger.Errorf("failed to decrypt password by aes key, error: %s", err) + return "", fmt.Errorf("invalid encrypted password") } - if len(plainText) == 0 { + if password == "" { return "", fmt.Errorf("invalid password") } - return plainText, nil + return password, nil } func LocalLogin(args *LoginArgs, logger *zap.SugaredLogger) (*User, int, error) { @@ -185,10 +197,7 @@ func LocalLogin(args *LoginArgs, logger *zap.SugaredLogger) (*User, int, error) return nil, 0, fmt.Errorf("account is required") } loginFailedKey := loginFailedCacheKey(account) - if strings.TrimSpace(args.Password) == "" { - return nil, 0, fmt.Errorf("password is required") - } - passwordText, err := decryptLoginPassword(args.Password, logger) + passwordText, err := decryptLoginPassword(args.EncryptedKey, args.Password, logger) if err != nil { return nil, 0, err } @@ -223,7 +232,7 @@ func LocalLogin(args *LoginArgs, logger *zap.SugaredLogger) (*User, int, error) failedCount = incrementLoginFailedCount(loginFailedKey, logger) return nil, failedCount, fmt.Errorf("invalid username or password") } - + password := []byte(passwordText) err = bcrypt.CompareHashAndPassword([]byte(userLogin.Password), password) if err == bcrypt.ErrMismatchedHashAndPassword {