Skip to content

Commit 31655aa

Browse files
guillep2klunny
authored andcommitted
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
1 parent 66e99d7 commit 31655aa

File tree

9 files changed

+140
-72
lines changed

9 files changed

+140
-72
lines changed

custom/conf/app.ini.sample

+2-1
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,8 @@ IMPORT_LOCAL_PATHS = false
345345
; Set to true to prevent all users (including admin) from creating custom git hooks
346346
DISABLE_GIT_HOOKS = false
347347
;Comma separated list of character classes required to pass minimum complexity.
348-
;If left empty or no valid values are specified, the default values (`lower,upper,digit,spec`) will be used.
348+
;If left empty or no valid values are specified, the default values ("lower,upper,digit,spec") will be used.
349+
;Use "off" to disable checking.
349350
PASSWORD_COMPLEXITY = lower,upper,digit,spec
350351
; Password Hash algorithm, either "pbkdf2", "argon2", "scrypt" or "bcrypt"
351352
PASSWORD_HASH_ALGO = pbkdf2

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,8 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
219219
- lower - use one or more lower latin characters
220220
- upper - use one or more upper latin characters
221221
- digit - use one or more digits
222-
- spec - use one or more special characters as ``][!"#$%&'()*+,./:;<=>?@\^_{|}~`-`` and space symbol.
222+
- spec - use one or more special characters as ``!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~``
223+
- off - do not check password complexity
223224

224225
## OpenID (`openid`)
225226

modules/password/password.go

+39-24
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,60 @@ package password
77
import (
88
"crypto/rand"
99
"math/big"
10-
"regexp"
10+
"strings"
1111
"sync"
1212

1313
"code.gitea.io/gitea/modules/setting"
1414
)
1515

16-
var matchComplexities = map[string]regexp.Regexp{}
17-
var matchComplexityOnce sync.Once
18-
var validChars string
19-
var validComplexities = map[string]string{
20-
"lower": "abcdefghijklmnopqrstuvwxyz",
21-
"upper": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
22-
"digit": "0123456789",
23-
"spec": `][ !"#$%&'()*+,./:;<=>?@\^_{|}~` + "`-",
24-
}
16+
var (
17+
matchComplexityOnce sync.Once
18+
validChars string
19+
requiredChars []string
20+
21+
charComplexities = map[string]string{
22+
"lower": `abcdefghijklmnopqrstuvwxyz`,
23+
"upper": `ABCDEFGHIJKLMNOPQRSTUVWXYZ`,
24+
"digit": `0123456789`,
25+
"spec": ` !"#$%&'()*+,-./:;<=>?@[\]^_{|}~` + "`",
26+
}
27+
)
2528

2629
// NewComplexity for preparation
2730
func NewComplexity() {
2831
matchComplexityOnce.Do(func() {
29-
if len(setting.PasswordComplexity) > 0 {
30-
for key, val := range setting.PasswordComplexity {
31-
matchComplexity := regexp.MustCompile(val)
32-
matchComplexities[key] = *matchComplexity
33-
validChars += validComplexities[key]
32+
setupComplexity(setting.PasswordComplexity)
33+
})
34+
}
35+
36+
func setupComplexity(values []string) {
37+
if len(values) != 1 || values[0] != "off" {
38+
for _, val := range values {
39+
if chars, ok := charComplexities[val]; ok {
40+
validChars += chars
41+
requiredChars = append(requiredChars, chars)
3442
}
35-
} else {
36-
for _, val := range validComplexities {
37-
validChars += val
43+
}
44+
if len(requiredChars) == 0 {
45+
// No valid character classes found; use all classes as default
46+
for _, chars := range charComplexities {
47+
validChars += chars
48+
requiredChars = append(requiredChars, chars)
3849
}
3950
}
40-
})
51+
}
52+
if validChars == "" {
53+
// No complexities to check; provide a sensible default for password generation
54+
validChars = charComplexities["lower"] + charComplexities["upper"] + charComplexities["digit"]
55+
}
4156
}
4257

43-
// IsComplexEnough return True if password is Complexity
58+
// IsComplexEnough return True if password meets complexity settings
4459
func IsComplexEnough(pwd string) bool {
45-
if len(setting.PasswordComplexity) > 0 {
46-
NewComplexity()
47-
for _, val := range matchComplexities {
48-
if !val.MatchString(pwd) {
60+
NewComplexity()
61+
if len(validChars) > 0 {
62+
for _, req := range requiredChars {
63+
if !strings.ContainsAny(req, pwd) {
4964
return false
5065
}
5166
}

modules/password/password_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package password
6+
7+
import (
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestComplexity_IsComplexEnough(t *testing.T) {
14+
matchComplexityOnce.Do(func() {})
15+
16+
testlist := []struct {
17+
complexity []string
18+
truevalues []string
19+
falsevalues []string
20+
}{
21+
{[]string{"lower"}, []string{"abc", "abc!"}, []string{"ABC", "123", "=!$", ""}},
22+
{[]string{"upper"}, []string{"ABC"}, []string{"abc", "123", "=!$", "abc!", ""}},
23+
{[]string{"digit"}, []string{"123"}, []string{"abc", "ABC", "=!$", "abc!", ""}},
24+
{[]string{"spec"}, []string{"=!$", "abc!"}, []string{"abc", "ABC", "123", ""}},
25+
{[]string{"off"}, []string{"abc", "ABC", "123", "=!$", "abc!", ""}, nil},
26+
{[]string{"lower", "spec"}, []string{"abc!"}, []string{"abc", "ABC", "123", "=!$", "abcABC123", ""}},
27+
{[]string{"lower", "upper", "digit"}, []string{"abcABC123"}, []string{"abc", "ABC", "123", "=!$", "abc!", ""}},
28+
}
29+
30+
for _, test := range testlist {
31+
testComplextity(test.complexity)
32+
for _, val := range test.truevalues {
33+
assert.True(t, IsComplexEnough(val))
34+
}
35+
for _, val := range test.falsevalues {
36+
assert.False(t, IsComplexEnough(val))
37+
}
38+
}
39+
40+
// Remove settings for other tests
41+
testComplextity([]string{"off"})
42+
}
43+
44+
func TestComplexity_Generate(t *testing.T) {
45+
matchComplexityOnce.Do(func() {})
46+
47+
const maxCount = 50
48+
const pwdLen = 50
49+
50+
test := func(t *testing.T, modes []string) {
51+
testComplextity(modes)
52+
for i := 0; i < maxCount; i++ {
53+
pwd, err := Generate(pwdLen)
54+
assert.NoError(t, err)
55+
assert.Equal(t, pwdLen, len(pwd))
56+
assert.True(t, IsComplexEnough(pwd), "Failed complexities with modes %+v for generated: %s", modes, pwd)
57+
}
58+
}
59+
60+
test(t, []string{"lower"})
61+
test(t, []string{"upper"})
62+
test(t, []string{"lower", "upper", "spec"})
63+
test(t, []string{"off"})
64+
test(t, []string{""})
65+
66+
// Remove settings for other tests
67+
testComplextity([]string{"off"})
68+
}
69+
70+
func testComplextity(values []string) {
71+
// Cleanup previous values
72+
validChars = ""
73+
requiredChars = make([]string, 0, len(values))
74+
setupComplexity(values)
75+
}

modules/setting/setting.go

+6-18
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ var (
149149
MinPasswordLength int
150150
ImportLocalPaths bool
151151
DisableGitHooks bool
152-
PasswordComplexity map[string]string
152+
PasswordComplexity []string
153153
PasswordHashAlgo string
154154

155155
// UI settings
@@ -781,26 +781,14 @@ func NewContext() {
781781

782782
InternalToken = loadInternalToken(sec)
783783

784-
var dictPC = map[string]string{
785-
"lower": "[a-z]+",
786-
"upper": "[A-Z]+",
787-
"digit": "[0-9]+",
788-
"spec": `][ !"#$%&'()*+,./:;<=>?@\\^_{|}~` + "`-",
789-
}
790-
PasswordComplexity = make(map[string]string)
791784
cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",")
792-
for _, y := range cfgdata {
793-
ts := strings.TrimSpace(y)
794-
for a := range dictPC {
795-
if strings.ToLower(ts) == a {
796-
PasswordComplexity[ts] = dictPC[ts]
797-
break
798-
}
785+
PasswordComplexity = make([]string, 0, len(cfgdata))
786+
for _, name := range cfgdata {
787+
name := strings.ToLower(strings.Trim(name, `"`))
788+
if name != "" {
789+
PasswordComplexity = append(PasswordComplexity, name)
799790
}
800791
}
801-
if len(PasswordComplexity) == 0 {
802-
PasswordComplexity = dictPC
803-
}
804792

805793
sec = Cfg.Section("attachment")
806794
AttachmentPath = sec.Key("PATH").MustString(path.Join(AppDataPath, "attachments"))

options/locale/locale_en-US.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ team_no_units_error = Allow access to at least one repository section.
315315
email_been_used = The email address is already used.
316316
openid_been_used = The OpenID address '%s' is already used.
317317
username_password_incorrect = Username or password is incorrect.
318-
password_complexity = Password does not pass complexity requirements.
318+
password_complexity = Password does not pass complexity requirements.
319319
enterred_invalid_repo_name = The repository name you entered is incorrect.
320320
enterred_invalid_owner_name = The new owner name is not valid.
321321
enterred_invalid_password = The password you entered is incorrect.

routers/admin/users_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func TestNewUserPost_MustChangePassword(t *testing.T) {
3434
LoginName: "local",
3535
UserName: username,
3636
Email: email,
37-
Password: "xxxxxxxx",
37+
Password: "abc123ABC!=$",
3838
SendNotify: false,
3939
MustChangePassword: true,
4040
}
@@ -71,7 +71,7 @@ func TestNewUserPost_MustChangePasswordFalse(t *testing.T) {
7171
LoginName: "local",
7272
UserName: username,
7373
Email: email,
74-
Password: "xxxxxxxx",
74+
Password: "abc123ABC!=$",
7575
SendNotify: false,
7676
MustChangePassword: false,
7777
}

routers/user/setting/account.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func AccountPost(ctx *context.Context, form auth.ChangePasswordForm) {
5454
} else if form.Password != form.Retype {
5555
ctx.Flash.Error(ctx.Tr("form.password_not_match"))
5656
} else if !password.IsComplexEnough(form.Password) {
57-
ctx.Flash.Error(ctx.Tr("settings.password_complexity"))
57+
ctx.Flash.Error(ctx.Tr("form.password_complexity"))
5858
} else {
5959
var err error
6060
if ctx.User.Salt, err = models.GetUserSalt(); err != nil {

routers/user/setting/account_test.go

+12-24
Original file line numberDiff line numberDiff line change
@@ -19,76 +19,64 @@ import (
1919
func TestChangePassword(t *testing.T) {
2020
oldPassword := "password"
2121
setting.MinPasswordLength = 6
22-
setting.PasswordComplexity = map[string]string{
23-
"lower": "[a-z]+",
24-
"upper": "[A-Z]+",
25-
"digit": "[0-9]+",
26-
"spec": "[-_]+",
27-
}
28-
var pcLUN = map[string]string{
29-
"lower": "[a-z]+",
30-
"upper": "[A-Z]+",
31-
"digit": "[0-9]+",
32-
}
33-
var pcLU = map[string]string{
34-
"lower": "[a-z]+",
35-
"upper": "[A-Z]+",
36-
}
22+
var pcALL = []string{"lower", "upper", "digit", "spec"}
23+
var pcLUN = []string{"lower", "upper", "digit"}
24+
var pcLU = []string{"lower", "upper"}
3725

3826
for _, req := range []struct {
3927
OldPassword string
4028
NewPassword string
4129
Retype string
4230
Message string
43-
PasswordComplexity map[string]string
31+
PasswordComplexity []string
4432
}{
4533
{
4634
OldPassword: oldPassword,
4735
NewPassword: "Qwerty123456-",
4836
Retype: "Qwerty123456-",
4937
Message: "",
50-
PasswordComplexity: setting.PasswordComplexity,
38+
PasswordComplexity: pcALL,
5139
},
5240
{
5341
OldPassword: oldPassword,
5442
NewPassword: "12345",
5543
Retype: "12345",
5644
Message: "auth.password_too_short",
57-
PasswordComplexity: setting.PasswordComplexity,
45+
PasswordComplexity: pcALL,
5846
},
5947
{
6048
OldPassword: "12334",
6149
NewPassword: "123456",
6250
Retype: "123456",
6351
Message: "settings.password_incorrect",
64-
PasswordComplexity: setting.PasswordComplexity,
52+
PasswordComplexity: pcALL,
6553
},
6654
{
6755
OldPassword: oldPassword,
6856
NewPassword: "123456",
6957
Retype: "12345",
7058
Message: "form.password_not_match",
71-
PasswordComplexity: setting.PasswordComplexity,
59+
PasswordComplexity: pcALL,
7260
},
7361
{
7462
OldPassword: oldPassword,
7563
NewPassword: "Qwerty",
7664
Retype: "Qwerty",
77-
Message: "settings.password_complexity",
78-
PasswordComplexity: setting.PasswordComplexity,
65+
Message: "form.password_complexity",
66+
PasswordComplexity: pcALL,
7967
},
8068
{
8169
OldPassword: oldPassword,
8270
NewPassword: "Qwerty",
8371
Retype: "Qwerty",
84-
Message: "settings.password_complexity",
72+
Message: "form.password_complexity",
8573
PasswordComplexity: pcLUN,
8674
},
8775
{
8876
OldPassword: oldPassword,
8977
NewPassword: "QWERTY",
9078
Retype: "QWERTY",
91-
Message: "settings.password_complexity",
79+
Message: "form.password_complexity",
9280
PasswordComplexity: pcLU,
9381
},
9482
} {

0 commit comments

Comments
 (0)