CVE-2023-4125: Weak Password Requirements (CWE-521)
value: Weak Password Requirements in GitHub repository answerdev/answer prior to v1.1.0.
CWE description: The product does not require that users should have strong passwords, which makes it easier for attackers to compromise user accounts.
CVSS v3 base score: 8.8
Published: 2023-08-03T04:15Z
Repository: https://github.com/answerdev/answer
Language: Go
Files changed: i18n/en_US.yaml, i18n/zh_CN.yaml, internal/base/validator/validator.go, internal/migrations/v10.go, internal/schema/user_schema.go, pkg/checker/password.go
Commit date: 2023-05-22 15:09:42 +0800
Commit message: fix(password): password can't contains space.

Diff: diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml
index b5fdcdc9f..0271ce768 100644
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@ -1,21 +1,6 @@
 # The following fields are used for back-end
-#  backend.email.tpl.change_email.title.other = Confirm your new email address for {{.SiteName}}  by clicking on the following link:\u003cbr\u003e\u003cbr\u003e\n\n\u003ca href='{{.ChangeEmailUrl}}' target='_blank'\u003e{{.ChangeEmailUrl}}\u003c/a\u003e\u003cbr\u003e\u003cbr\u003e\n\nIf you did not request this change, please ignore this email.\n
-#  backend.email.tpl.change_email.body.other = [{{.SiteName}}] Confirm your new email address
-#  backend.email.tpl.new_answer.title.other = \u003cstrong\u003e\u003ca href='{{.AnswerUrl}}'\u003e{{.QuestionTitle}}\u003c/a\u003e\u003c/strong\u003e\u003cbr\u003e\u003cbr\u003e\n\n\u003csmall\u003e{{.DisplayName}}:\u003c/small\u003e\u003cbr\u003e\n\u003cblockquote\u003e{{.AnswerSummary}}\u003c/blockquote\u003e\u003cbr\u003e\n\u003ca href='{{.AnswerUrl}}'\u003eView it on {{.SiteName}}\u003c/a\u003e\u003cbr\u003e\u003cbr\u003e\n\n\u003csmall\u003eYou are receiving this because you authored the thread. \u003ca href='{{.UnsubscribeUrl}}'\u003eUnsubscribe\u003c/a\u003e\u003c/small\u003e
-#  backend.email.tpl.new_answer.body.other = [{{.SiteName}}] {{.DisplayName}} answered your question
-#  backend.email.tpl.new_comment.title.other = \u003cstrong\u003e\u003ca href='{{.CommentUrl}}'\u003e{{.QuestionTitle}}\u003c/a\u003e\u003c/strong\u003e\u003cbr\u003e\u003cbr\u003e\n\n\u003csmall\u003e{{.DisplayName}}:\u003c/small\u003e\u003cbr\u003e\n\u003cblockquote\u003e{{.CommentSummary}}\u003c/blockquote\u003e\u003cbr\u003e\n\u003ca href='{{.CommentUrl}}'\u003eView it on {{.SiteName}}\u003c/a\u003e\u003cbr\u003e\u003cbr\u003e\n\n\u003csmall\u003eYou are receiving this because you authored the thread. \u003ca href='{{.UnsubscribeUrl}}'\u003eUnsubscribe\u003c/a\u003e\u003c/small\u003e
-#  backend.email.tpl.new_comment.body.other = [{{.SiteName}}] {{.DisplayName}} commented on your post
-#  backend.email.tpl.pass_reset.title.other = Somebody asked to reset your password on [{{.SiteName}}].\u003cbr\u003e\u003cbr\u003e\n\nIf it was not you, you can safely ignore this email.\u003cbr\u003e\u003cbr\u003e\n\nClick the following link to choose a new password:\u003cbr\u003e\n\u003ca href='{{.PassResetUrl}}' target='_blank'\u003e{{.PassResetUrl}}\u003c/a\u003e\n
-#  backend.email.tpl.pass_reset.body.other = [{{.SiteName }}] Password reset
-#  backend.email.tpl.register.title.other = Welcome to {{.SiteName}}\u003cbr\u003e\u003cbr\u003e\n\nClick the following link to confirm and activate your new account:\u003cbr\u003e\n\u003ca href='{{.RegisterUrl}}' target='_blank'\u003e{{.RegisterUrl}}\u003c/a\u003e\u003cbr\u003e\u003cbr\u003e\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n
-#  backend.email.tpl.register.body.other = [{{.SiteName}}] Confirm your new account
-#  backend.email.tpl.test.title.other = This is a test email.
-#  backend.email.tpl.test.body.other = [{{.SiteName}}] Test Email
-
-
-
 backend:
   base:
     success:
       other: Success.
     unknown:
@@ -121,10 +106,13 @@ backend:
   password:
     other: Password
   email_or_password_wrong_error:
     other: Email and password do not match.
   error:
+    password:
+      space_invalid:
+        other: Password cannot contain spaces.
     admin:
       cannot_update_their_password:
         other: You cannot modify your password.
       cannot_modify_self_status:
         other: You cannot modify your status.
diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml
index 29ca8cbb1..84036e104 100644
--- a/i18n/zh_CN.yaml
+++ b/i18n/zh_CN.yaml
@@ -95,17 +95,20 @@ backend:
       other: 审核标签编辑
     rank_tag_edit_without_review_label:
       other: 编辑标签描述（无需审核）
     rank_tag_synonym_label:
       other: 管理标签同义词
-  email:
+  e_mail:
     other: 邮箱
   password:
     other: 密码
   email_or_password_wrong_error:
     other: 邮箱和密码不匹配。
   error:
+    password:
+      space_invalid:
+        other: 密码不能包含空格。
     admin:
       cannot_update_their_password:
         other: 您无法修改自己的密码。
       cannot_modify_self_status:
         other: 您无法修改自己的状态。
diff --git a/internal/base/validator/validator.go b/internal/base/validator/validator.go
index 3ce824a4f..9f46d229a 100644
--- a/internal/base/validator/validator.go
+++ b/internal/base/validator/validator.go
@@ -205,14 +205,16 @@ func (m *MyValidator) Check(value interface{}) (errFields []*FormErrorField, err
 	if v, ok := value.(Checker); ok {
 		errFields, err = v.Check()
 		if err == nil {
 			return nil, nil
 		}
+		errMsg := ""
 		for _, errField := range errFields {
 			errField.ErrorMsg = translator.Tr(m.Lang, errField.ErrorMsg)
+			errMsg = errField.ErrorMsg
 		}
-		return errFields, err
+		return errFields, myErrors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
 	}
 	return nil, nil
 }
 
 // Checker .
diff --git a/internal/migrations/v10.go b/internal/migrations/v10.go
index 7ad0c9906..2d669b192 100644
--- a/internal/migrations/v10.go
+++ b/internal/migrations/v10.go
@@ -22,10 +22,12 @@ func addLoginLimitations(x *xorm.Engine) error {
 	if exist {
 		content := &schema.SiteLoginReq{}
 		_ = json.Unmarshal([]byte(loginSiteInfo.Content), content)
 		content.AllowEmailRegistrations = true
 		content.AllowEmailDomains = make([]string, 0)
+		data, _ := json.Marshal(content)
+		loginSiteInfo.Content = string(data)
 		_, err = x.ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo)
 		if err != nil {
 			return fmt.Errorf("update site info failed: %w", err)
 		}
 	}
diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go
index f1f2bc2f0..f0bdafff1 100644
--- a/internal/schema/user_schema.go
+++ b/internal/schema/user_schema.go
@@ -260,39 +260,33 @@ type UserRegisterReq struct {
 	CaptchaID   string `json:"captcha_id"`   // captcha_id
 	CaptchaCode string `json:"captcha_code"` // captcha_code
 }
 
 func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err error) {
-	// TODO i18n
-	err = checker.CheckPassword(8, 32, 0, u.Pass)
-	if err != nil {
-		errField := &validator.FormErrorField{
+	if err = checker.CheckPassword(u.Pass); err != nil {
+		errFields = append(errFields, &validator.FormErrorField{
 			ErrorField: "pass",
 			ErrorMsg:   err.Error(),
-		}
-		errFields = append(errFields, errField)
+		})
 		return errFields, err
 	}
 	return nil, nil
 }
 
 type UserModifyPasswordReq struct {
-	OldPass string `validate:"omitempty,gte=8,lte=32" json:"old_pass"`
-	Pass    string `validate:"required,gte=8,lte=32" json:"pass"`
+	OldPass     string `validate:"omitempty,gte=8,lte=32" json:"old_pass"`
+	Pass        string `validate:"required,gte=8,lte=32" json:"pass"`
 	UserID      string `json:"-"`
 	AccessToken string `json:"-"`
 }
 
 func (u *UserModifyPasswordReq) Check() (errFields []*validator.FormErrorField, err error) {
-	// TODO i18n
-	err = checker.CheckPassword(8, 32, 0, u.Pass)
-	if err != nil {
-		errField := &validator.FormErrorField{
+	if err = checker.CheckPassword(u.Pass); err != nil {
+		errFields = append(errFields, &validator.FormErrorField{
 			ErrorField: "pass",
 			ErrorMsg:   err.Error(),
-		}
-		errFields = append(errFields, errField)
+		})
 		return errFields, err
 	}
 	return nil, nil
 }
 
@@ -350,18 +344,15 @@ type UserRePassWordRequest struct {
 	Pass    string `validate:"required,gt=0,lte=32" json:"pass" `  // Password
 	Content string `json:"-"`
 }
 
 func (u *UserRePassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {
-	// TODO i18n
-	err = checker.CheckPassword(8, 32, 0, u.Pass)
-	if err != nil {
-		errField := &validator.FormErrorField{
+	if err = checker.CheckPassword(u.Pass); err != nil {
+		errFields = append(errFields, &validator.FormErrorField{
 			ErrorField: "pass",
 			ErrorMsg:   err.Error(),
-		}
-		errFields = append(errFields, errField)
+		})
 		return errFields, err
 	}
 	return nil, nil
 }
 
diff --git a/pkg/checker/password.go b/pkg/checker/password.go
index 966b851c4..34321350f 100644
--- a/pkg/checker/password.go
+++ b/pkg/checker/password.go
@@ -1,41 +1,41 @@
 package checker
 
 import (
 	"fmt"
 	"regexp"
+	"strings"
 )
 
 const (
 	levelD = iota
 	LevelC
 	LevelB
 	LevelA
 	LevelS
 )
 
-// CheckPassword
-// minLength: Specifies the minimum length of a password
-// maxLength：Specifies the maximum length of a password
-// minLevel：Specifies the minimum strength level required for passwords
-// pwd：Text passwords
-func CheckPassword(minLength, maxLength, minLevel int, pwd string) error {
-	// First check whether the password length is within the range
-	if len(pwd) < minLength {
-		return fmt.Errorf("BAD PASSWORD: The password is shorter than %d characters", minLength)
-	}
-	if len(pwd) > maxLength {
-		return fmt.Errorf("BAD PASSWORD: The password is logner than %d characters", maxLength)
+const (
+	PasswordCannotContainSpaces = "error.password.space_invalid"
+)
+
+// CheckPassword checks the password strength
+func CheckPassword(password string) error {
+	if strings.Contains(password, " ") {
+		return fmt.Errorf(PasswordCannotContainSpaces)
 	}
 
+	// TODO Currently there is no requirement for password strength
+	minLevel := 0
+
 	// The password strength level is initialized to D.
 	// The regular is used to verify the password strength.
 	// If the matching is successful, the password strength increases by 1
 	level := levelD
 	patternList := []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[~!@#$%^&*?_-]+`}
 	for _, pattern := range patternList {
-		match, _ := regexp.MatchString(pattern, pwd)
+		match, _ := regexp.MatchString(pattern, password)
 		if match {
 			level++
 		}
 	}
 

Vulnerable code: #  backend.email.tpl.change_email.title.other = Confirm your new email address for {{.SiteName}}  by clicking on the following link:\u003cbr\u003e\u003cbr\u003e\n\n\u003ca href='{{.ChangeEmailUrl}}' target='_blank'\u003e{{.ChangeEmailUrl}}\u003c/a\u003e\u003cbr\u003e\u003cbr\u003e\n\nIf you did not request this change, please ignore this email.\n
#  backend.email.tpl.change_email.body.other = [{{.SiteName}}] Confirm your new email address
#  backend.email.tpl.new_answer.title.other = \u003cstrong\u003e\u003ca href='{{.AnswerUrl}}'\u003e{{.QuestionTitle}}\u003c/a\u003e\u003c/strong\u003e\u003cbr\u003e\u003cbr\u003e\n\n\u003csmall\u003e{{.DisplayName}}:\u003c/small\u003e\u003cbr\u003e\n\u003cblockquote\u003e{{.AnswerSummary}}\u003c/blockquote\u003e\u003cbr\u003e\n\u003ca href='{{.AnswerUrl}}'\u003eView it on {{.SiteName}}\u003c/a\u003e\u003cbr\u003e\u003cbr\u003e\n\n\u003csmall\u003eYou are receiving this because you authored the thread. \u003ca href='{{.UnsubscribeUrl}}'\u003eUnsubscribe\u003c/a\u003e\u003c/small\u003e
#  backend.email.tpl.new_answer.body.other = [{{.SiteName}}] {{.DisplayName}} answered your question
#  backend.email.tpl.new_comment.title.other = \u003cstrong\u003e\u003ca href='{{.CommentUrl}}'\u003e{{.QuestionTitle}}\u003c/a\u003e\u003c/strong\u003e\u003cbr\u003e\u003cbr\u003e\n\n\u003csmall\u003e{{.DisplayName}}:\u003c/small\u003e\u003cbr\u003e\n\u003cblockquote\u003e{{.CommentSummary}}\u003c/blockquote\u003e\u003cbr\u003e\n\u003ca href='{{.CommentUrl}}'\u003eView it on {{.SiteName}}\u003c/a\u003e\u003cbr\u003e\u003cbr\u003e\n\n\u003csmall\u003eYou are receiving this because you authored the thread. \u003ca href='{{.UnsubscribeUrl}}'\u003eUnsubscribe\u003c/a\u003e\u003c/small\u003e
#  backend.email.tpl.new_comment.body.other = [{{.SiteName}}] {{.DisplayName}} commented on your post
#  backend.email.tpl.pass_reset.title.other = Somebody asked to reset your password on [{{.SiteName}}].\u003cbr\u003e\u003cbr\u003e\n\nIf it was not you, you can safely ignore this email.\u003cbr\u003e\u003cbr\u003e\n\nClick the following link to choose a new password:\u003cbr\u003e\n\u003ca href='{{.PassResetUrl}}' target='_blank'\u003e{{.PassResetUrl}}\u003c/a\u003e\n
#  backend.email.tpl.pass_reset.body.other = [{{.SiteName }}] Password reset
#  backend.email.tpl.register.title.other = Welcome to {{.SiteName}}\u003cbr\u003e\u003cbr\u003e\n\nClick the following link to confirm and activate your new account:\u003cbr\u003e\n\u003ca href='{{.RegisterUrl}}' target='_blank'\u003e{{.RegisterUrl}}\u003c/a\u003e\u003cbr\u003e\u003cbr\u003e\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n
#  backend.email.tpl.register.body.other = [{{.SiteName}}] Confirm your new account
#  backend.email.tpl.test.title.other = This is a test email.
#  backend.email.tpl.test.body.other = [{{.SiteName}}] Test Email



  email:
		return errFields, err
	// TODO i18n
	err = checker.CheckPassword(8, 32, 0, u.Pass)
	if err != nil {
		errField := &validator.FormErrorField{
		}
		errFields = append(errFields, errField)
	OldPass string `validate:"omitempty,gte=8,lte=32" json:"old_pass"`
	Pass    string `validate:"required,gte=8,lte=32" json:"pass"`
	// TODO i18n
	err = checker.CheckPassword(8, 32, 0, u.Pass)
	if err != nil {
		errField := &validator.FormErrorField{
		}
		errFields = append(errFields, errField)
	// TODO i18n
	err = checker.CheckPassword(8, 32, 0, u.Pass)
	if err != nil {
		errField := &validator.FormErrorField{
		}
		errFields = append(errFields, errField)
// CheckPassword
// minLength: Specifies the minimum length of a password
// maxLength：Specifies the maximum length of a password
// minLevel：Specifies the minimum strength level required for passwords
// pwd：Text passwords
func CheckPassword(minLength, maxLength, minLevel int, pwd string) error {
	// First check whether the password length is within the range
	if len(pwd) < minLength {
		return fmt.Errorf("BAD PASSWORD: The password is shorter than %d characters", minLength)
	}
	if len(pwd) > maxLength {
		return fmt.Errorf("BAD PASSWORD: The password is logner than %d characters", maxLength)
		match, _ := regexp.MatchString(pattern, pwd)
Fixed code:     password:
      space_invalid:
        other: Password cannot contain spaces.
  e_mail:
    password:
      space_invalid:
        other: 密码不能包含空格。
		errMsg := ""
			errMsg = errField.ErrorMsg
		return errFields, myErrors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
		data, _ := json.Marshal(content)
		loginSiteInfo.Content = string(data)
	if err = checker.CheckPassword(u.Pass); err != nil {
		errFields = append(errFields, &validator.FormErrorField{
		})
	OldPass     string `validate:"omitempty,gte=8,lte=32" json:"old_pass"`
	Pass        string `validate:"required,gte=8,lte=32" json:"pass"`
	if err = checker.CheckPassword(u.Pass); err != nil {
		errFields = append(errFields, &validator.FormErrorField{
		})
	if err = checker.CheckPassword(u.Pass); err != nil {
		errFields = append(errFields, &validator.FormErrorField{
		})
	"strings"
const (
	PasswordCannotContainSpaces = "error.password.space_invalid"
)

// CheckPassword checks the password strength
func CheckPassword(password string) error {
	if strings.Contains(password, " ") {
		return fmt.Errorf(PasswordCannotContainSpaces)
	// TODO Currently there is no requirement for password strength
	minLevel := 0

		match, _ := regexp.MatchString(pattern, password)