Fix password complexity regex for special characters (on master) (#8525)
* Fix extra space * Fix regular expression * Fix error template name * Simplify check code, fix default values, add test * Fix router tests * Fix fmt * Fix setting and lint * Move cleaning up code to test, improve comments * Tidy up variable declaration
This commit is contained in:
		
							parent
							
								
									66e99d722a
								
							
						
					
					
						commit
						31655aabfc
					
				
					 9 changed files with 142 additions and 74 deletions
				
			
		|  | @ -345,7 +345,8 @@ IMPORT_LOCAL_PATHS = false | |||
| ; Set to true to prevent all users (including admin) from creating custom git hooks | ||||
| DISABLE_GIT_HOOKS = false | ||||
| ;Comma separated list of character classes required to pass minimum complexity. | ||||
| ;If left empty or no valid values are specified, the default values (`lower,upper,digit,spec`) will be used. | ||||
| ;If left empty or no valid values are specified, the default values ("lower,upper,digit,spec") will be used. | ||||
| ;Use "off" to disable checking. | ||||
| PASSWORD_COMPLEXITY = lower,upper,digit,spec | ||||
| ; Password Hash algorithm, either "pbkdf2", "argon2", "scrypt" or "bcrypt" | ||||
| PASSWORD_HASH_ALGO = pbkdf2 | ||||
|  |  | |||
|  | @ -219,7 +219,8 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. | |||
|     - lower - use one or more lower latin characters | ||||
|     - upper - use one or more upper latin characters | ||||
|     - digit - use one or more digits | ||||
|     - spec - use one or more special characters as ``][!"#$%&'()*+,./:;<=>?@\^_{|}~`-`` and space symbol.  | ||||
|     - spec - use one or more special characters as ``!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~`` | ||||
|     - off - do not check password complexity | ||||
| 
 | ||||
| ## OpenID (`openid`) | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,45 +7,60 @@ package password | |||
| import ( | ||||
| 	"crypto/rand" | ||||
| 	"math/big" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| ) | ||||
| 
 | ||||
| var matchComplexities = map[string]regexp.Regexp{} | ||||
| var matchComplexityOnce sync.Once | ||||
| var validChars string | ||||
| var validComplexities = map[string]string{ | ||||
| 	"lower": "abcdefghijklmnopqrstuvwxyz", | ||||
| 	"upper": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", | ||||
| 	"digit": "0123456789", | ||||
| 	"spec":  `][ !"#$%&'()*+,./:;<=>?@\^_{|}~` + "`-", | ||||
| } | ||||
| var ( | ||||
| 	matchComplexityOnce sync.Once | ||||
| 	validChars          string | ||||
| 	requiredChars       []string | ||||
| 
 | ||||
| 	charComplexities = map[string]string{ | ||||
| 		"lower": `abcdefghijklmnopqrstuvwxyz`, | ||||
| 		"upper": `ABCDEFGHIJKLMNOPQRSTUVWXYZ`, | ||||
| 		"digit": `0123456789`, | ||||
| 		"spec":  ` !"#$%&'()*+,-./:;<=>?@[\]^_{|}~` + "`", | ||||
| 	} | ||||
| ) | ||||
| 
 | ||||
| // NewComplexity for preparation
 | ||||
| func NewComplexity() { | ||||
| 	matchComplexityOnce.Do(func() { | ||||
| 		if len(setting.PasswordComplexity) > 0 { | ||||
| 			for key, val := range setting.PasswordComplexity { | ||||
| 				matchComplexity := regexp.MustCompile(val) | ||||
| 				matchComplexities[key] = *matchComplexity | ||||
| 				validChars += validComplexities[key] | ||||
| 			} | ||||
| 		} else { | ||||
| 			for _, val := range validComplexities { | ||||
| 				validChars += val | ||||
| 			} | ||||
| 		} | ||||
| 		setupComplexity(setting.PasswordComplexity) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // IsComplexEnough return True if password is Complexity
 | ||||
| func setupComplexity(values []string) { | ||||
| 	if len(values) != 1 || values[0] != "off" { | ||||
| 		for _, val := range values { | ||||
| 			if chars, ok := charComplexities[val]; ok { | ||||
| 				validChars += chars | ||||
| 				requiredChars = append(requiredChars, chars) | ||||
| 			} | ||||
| 		} | ||||
| 		if len(requiredChars) == 0 { | ||||
| 			// No valid character classes found; use all classes as default
 | ||||
| 			for _, chars := range charComplexities { | ||||
| 				validChars += chars | ||||
| 				requiredChars = append(requiredChars, chars) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if validChars == "" { | ||||
| 		// No complexities to check; provide a sensible default for password generation
 | ||||
| 		validChars = charComplexities["lower"] + charComplexities["upper"] + charComplexities["digit"] | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // IsComplexEnough return True if password meets complexity settings
 | ||||
| func IsComplexEnough(pwd string) bool { | ||||
| 	if len(setting.PasswordComplexity) > 0 { | ||||
| 		NewComplexity() | ||||
| 		for _, val := range matchComplexities { | ||||
| 			if !val.MatchString(pwd) { | ||||
| 	NewComplexity() | ||||
| 	if len(validChars) > 0 { | ||||
| 		for _, req := range requiredChars { | ||||
| 			if !strings.ContainsAny(req, pwd) { | ||||
| 				return false | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
							
								
								
									
										75
									
								
								modules/password/password_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								modules/password/password_test.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,75 @@ | |||
| // Copyright 2019 The Gitea Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a MIT-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package password | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestComplexity_IsComplexEnough(t *testing.T) { | ||||
| 	matchComplexityOnce.Do(func() {}) | ||||
| 
 | ||||
| 	testlist := []struct { | ||||
| 		complexity  []string | ||||
| 		truevalues  []string | ||||
| 		falsevalues []string | ||||
| 	}{ | ||||
| 		{[]string{"lower"}, []string{"abc", "abc!"}, []string{"ABC", "123", "=!$", ""}}, | ||||
| 		{[]string{"upper"}, []string{"ABC"}, []string{"abc", "123", "=!$", "abc!", ""}}, | ||||
| 		{[]string{"digit"}, []string{"123"}, []string{"abc", "ABC", "=!$", "abc!", ""}}, | ||||
| 		{[]string{"spec"}, []string{"=!$", "abc!"}, []string{"abc", "ABC", "123", ""}}, | ||||
| 		{[]string{"off"}, []string{"abc", "ABC", "123", "=!$", "abc!", ""}, nil}, | ||||
| 		{[]string{"lower", "spec"}, []string{"abc!"}, []string{"abc", "ABC", "123", "=!$", "abcABC123", ""}}, | ||||
| 		{[]string{"lower", "upper", "digit"}, []string{"abcABC123"}, []string{"abc", "ABC", "123", "=!$", "abc!", ""}}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, test := range testlist { | ||||
| 		testComplextity(test.complexity) | ||||
| 		for _, val := range test.truevalues { | ||||
| 			assert.True(t, IsComplexEnough(val)) | ||||
| 		} | ||||
| 		for _, val := range test.falsevalues { | ||||
| 			assert.False(t, IsComplexEnough(val)) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Remove settings for other tests
 | ||||
| 	testComplextity([]string{"off"}) | ||||
| } | ||||
| 
 | ||||
| func TestComplexity_Generate(t *testing.T) { | ||||
| 	matchComplexityOnce.Do(func() {}) | ||||
| 
 | ||||
| 	const maxCount = 50 | ||||
| 	const pwdLen = 50 | ||||
| 
 | ||||
| 	test := func(t *testing.T, modes []string) { | ||||
| 		testComplextity(modes) | ||||
| 		for i := 0; i < maxCount; i++ { | ||||
| 			pwd, err := Generate(pwdLen) | ||||
| 			assert.NoError(t, err) | ||||
| 			assert.Equal(t, pwdLen, len(pwd)) | ||||
| 			assert.True(t, IsComplexEnough(pwd), "Failed complexities with modes %+v for generated: %s", modes, pwd) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	test(t, []string{"lower"}) | ||||
| 	test(t, []string{"upper"}) | ||||
| 	test(t, []string{"lower", "upper", "spec"}) | ||||
| 	test(t, []string{"off"}) | ||||
| 	test(t, []string{""}) | ||||
| 
 | ||||
| 	// Remove settings for other tests
 | ||||
| 	testComplextity([]string{"off"}) | ||||
| } | ||||
| 
 | ||||
| func testComplextity(values []string) { | ||||
| 	// Cleanup previous values
 | ||||
| 	validChars = "" | ||||
| 	requiredChars = make([]string, 0, len(values)) | ||||
| 	setupComplexity(values) | ||||
| } | ||||
|  | @ -149,7 +149,7 @@ var ( | |||
| 	MinPasswordLength     int | ||||
| 	ImportLocalPaths      bool | ||||
| 	DisableGitHooks       bool | ||||
| 	PasswordComplexity    map[string]string | ||||
| 	PasswordComplexity    []string | ||||
| 	PasswordHashAlgo      string | ||||
| 
 | ||||
| 	// UI settings
 | ||||
|  | @ -781,26 +781,14 @@ func NewContext() { | |||
| 
 | ||||
| 	InternalToken = loadInternalToken(sec) | ||||
| 
 | ||||
| 	var dictPC = map[string]string{ | ||||
| 		"lower": "[a-z]+", | ||||
| 		"upper": "[A-Z]+", | ||||
| 		"digit": "[0-9]+", | ||||
| 		"spec":  `][ !"#$%&'()*+,./:;<=>?@\\^_{|}~` + "`-", | ||||
| 	} | ||||
| 	PasswordComplexity = make(map[string]string) | ||||
| 	cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",") | ||||
| 	for _, y := range cfgdata { | ||||
| 		ts := strings.TrimSpace(y) | ||||
| 		for a := range dictPC { | ||||
| 			if strings.ToLower(ts) == a { | ||||
| 				PasswordComplexity[ts] = dictPC[ts] | ||||
| 				break | ||||
| 			} | ||||
| 	PasswordComplexity = make([]string, 0, len(cfgdata)) | ||||
| 	for _, name := range cfgdata { | ||||
| 		name := strings.ToLower(strings.Trim(name, `"`)) | ||||
| 		if name != "" { | ||||
| 			PasswordComplexity = append(PasswordComplexity, name) | ||||
| 		} | ||||
| 	} | ||||
| 	if len(PasswordComplexity) == 0 { | ||||
| 		PasswordComplexity = dictPC | ||||
| 	} | ||||
| 
 | ||||
| 	sec = Cfg.Section("attachment") | ||||
| 	AttachmentPath = sec.Key("PATH").MustString(path.Join(AppDataPath, "attachments")) | ||||
|  |  | |||
|  | @ -315,7 +315,7 @@ team_no_units_error = Allow access to at least one repository section. | |||
| email_been_used = The email address is already used. | ||||
| openid_been_used = The OpenID address '%s' is already used. | ||||
| username_password_incorrect = Username or password is incorrect. | ||||
| password_complexity =  Password does not pass complexity requirements. | ||||
| password_complexity = Password does not pass complexity requirements. | ||||
| enterred_invalid_repo_name = The repository name you entered is incorrect. | ||||
| enterred_invalid_owner_name = The new owner name is not valid. | ||||
| enterred_invalid_password = The password you entered is incorrect. | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ func TestNewUserPost_MustChangePassword(t *testing.T) { | |||
| 		LoginName:          "local", | ||||
| 		UserName:           username, | ||||
| 		Email:              email, | ||||
| 		Password:           "xxxxxxxx", | ||||
| 		Password:           "abc123ABC!=$", | ||||
| 		SendNotify:         false, | ||||
| 		MustChangePassword: true, | ||||
| 	} | ||||
|  | @ -71,7 +71,7 @@ func TestNewUserPost_MustChangePasswordFalse(t *testing.T) { | |||
| 		LoginName:          "local", | ||||
| 		UserName:           username, | ||||
| 		Email:              email, | ||||
| 		Password:           "xxxxxxxx", | ||||
| 		Password:           "abc123ABC!=$", | ||||
| 		SendNotify:         false, | ||||
| 		MustChangePassword: false, | ||||
| 	} | ||||
|  |  | |||
|  | @ -54,7 +54,7 @@ func AccountPost(ctx *context.Context, form auth.ChangePasswordForm) { | |||
| 	} else if form.Password != form.Retype { | ||||
| 		ctx.Flash.Error(ctx.Tr("form.password_not_match")) | ||||
| 	} else if !password.IsComplexEnough(form.Password) { | ||||
| 		ctx.Flash.Error(ctx.Tr("settings.password_complexity")) | ||||
| 		ctx.Flash.Error(ctx.Tr("form.password_complexity")) | ||||
| 	} else { | ||||
| 		var err error | ||||
| 		if ctx.User.Salt, err = models.GetUserSalt(); err != nil { | ||||
|  |  | |||
|  | @ -19,76 +19,64 @@ import ( | |||
| func TestChangePassword(t *testing.T) { | ||||
| 	oldPassword := "password" | ||||
| 	setting.MinPasswordLength = 6 | ||||
| 	setting.PasswordComplexity = map[string]string{ | ||||
| 		"lower": "[a-z]+", | ||||
| 		"upper": "[A-Z]+", | ||||
| 		"digit": "[0-9]+", | ||||
| 		"spec":  "[-_]+", | ||||
| 	} | ||||
| 	var pcLUN = map[string]string{ | ||||
| 		"lower": "[a-z]+", | ||||
| 		"upper": "[A-Z]+", | ||||
| 		"digit": "[0-9]+", | ||||
| 	} | ||||
| 	var pcLU = map[string]string{ | ||||
| 		"lower": "[a-z]+", | ||||
| 		"upper": "[A-Z]+", | ||||
| 	} | ||||
| 	var pcALL = []string{"lower", "upper", "digit", "spec"} | ||||
| 	var pcLUN = []string{"lower", "upper", "digit"} | ||||
| 	var pcLU = []string{"lower", "upper"} | ||||
| 
 | ||||
| 	for _, req := range []struct { | ||||
| 		OldPassword        string | ||||
| 		NewPassword        string | ||||
| 		Retype             string | ||||
| 		Message            string | ||||
| 		PasswordComplexity map[string]string | ||||
| 		PasswordComplexity []string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			OldPassword:        oldPassword, | ||||
| 			NewPassword:        "Qwerty123456-", | ||||
| 			Retype:             "Qwerty123456-", | ||||
| 			Message:            "", | ||||
| 			PasswordComplexity: setting.PasswordComplexity, | ||||
| 			PasswordComplexity: pcALL, | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldPassword:        oldPassword, | ||||
| 			NewPassword:        "12345", | ||||
| 			Retype:             "12345", | ||||
| 			Message:            "auth.password_too_short", | ||||
| 			PasswordComplexity: setting.PasswordComplexity, | ||||
| 			PasswordComplexity: pcALL, | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldPassword:        "12334", | ||||
| 			NewPassword:        "123456", | ||||
| 			Retype:             "123456", | ||||
| 			Message:            "settings.password_incorrect", | ||||
| 			PasswordComplexity: setting.PasswordComplexity, | ||||
| 			PasswordComplexity: pcALL, | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldPassword:        oldPassword, | ||||
| 			NewPassword:        "123456", | ||||
| 			Retype:             "12345", | ||||
| 			Message:            "form.password_not_match", | ||||
| 			PasswordComplexity: setting.PasswordComplexity, | ||||
| 			PasswordComplexity: pcALL, | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldPassword:        oldPassword, | ||||
| 			NewPassword:        "Qwerty", | ||||
| 			Retype:             "Qwerty", | ||||
| 			Message:            "settings.password_complexity", | ||||
| 			PasswordComplexity: setting.PasswordComplexity, | ||||
| 			Message:            "form.password_complexity", | ||||
| 			PasswordComplexity: pcALL, | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldPassword:        oldPassword, | ||||
| 			NewPassword:        "Qwerty", | ||||
| 			Retype:             "Qwerty", | ||||
| 			Message:            "settings.password_complexity", | ||||
| 			Message:            "form.password_complexity", | ||||
| 			PasswordComplexity: pcLUN, | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldPassword:        oldPassword, | ||||
| 			NewPassword:        "QWERTY", | ||||
| 			Retype:             "QWERTY", | ||||
| 			Message:            "settings.password_complexity", | ||||
| 			Message:            "form.password_complexity", | ||||
| 			PasswordComplexity: pcLU, | ||||
| 		}, | ||||
| 	} { | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue