mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Improve valid user name check (#20136)
Close https://github.com/go-gitea/gitea/issues/21640 Before: Gitea can create users like ".xxx" or "x..y", which is not ideal, it's already a consensus that dot filenames have special meanings, and `a..b` is a confusing name when doing cross repo compare. After: stricter Co-authored-by: Jason Song <i@wolfogre.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: delvh <dev.lh@web.de>
This commit is contained in:
		@@ -29,6 +29,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/validation"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/argon2"
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
@@ -621,7 +622,7 @@ var (
 | 
			
		||||
// IsUsableUsername returns an error when a username is reserved
 | 
			
		||||
func IsUsableUsername(name string) error {
 | 
			
		||||
	// Validate username make sure it satisfies requirement.
 | 
			
		||||
	if db.AlphaDashDotPattern.MatchString(name) {
 | 
			
		||||
	if !validation.IsValidUsername(name) {
 | 
			
		||||
		// Note: usually this error is normally caught up earlier in the UI
 | 
			
		||||
		return db.ErrNameCharsNotAllowed{Name: name}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ type CreateUserOption struct {
 | 
			
		||||
	SourceID  int64  `json:"source_id"`
 | 
			
		||||
	LoginName string `json:"login_name"`
 | 
			
		||||
	// required: true
 | 
			
		||||
	Username string `json:"username" binding:"Required;AlphaDashDot;MaxSize(40)"`
 | 
			
		||||
	Username string `json:"username" binding:"Required;Username;MaxSize(40)"`
 | 
			
		||||
	FullName string `json:"full_name" binding:"MaxSize(100)"`
 | 
			
		||||
	// required: true
 | 
			
		||||
	// swagger:strfmt email
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,9 @@ const (
 | 
			
		||||
 | 
			
		||||
	// ErrRegexPattern is returned when a regex pattern is invalid
 | 
			
		||||
	ErrRegexPattern = "RegexPattern"
 | 
			
		||||
 | 
			
		||||
	// ErrUsername is username error
 | 
			
		||||
	ErrUsername = "UsernameError"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AddBindingRules adds additional binding rules
 | 
			
		||||
@@ -34,6 +37,7 @@ func AddBindingRules() {
 | 
			
		||||
	addGlobPatternRule()
 | 
			
		||||
	addRegexPatternRule()
 | 
			
		||||
	addGlobOrRegexPatternRule()
 | 
			
		||||
	addUsernamePatternRule()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func addGitRefNameBindingRule() {
 | 
			
		||||
@@ -148,6 +152,22 @@ func addGlobOrRegexPatternRule() {
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func addUsernamePatternRule() {
 | 
			
		||||
	binding.AddRule(&binding.Rule{
 | 
			
		||||
		IsMatch: func(rule string) bool {
 | 
			
		||||
			return rule == "Username"
 | 
			
		||||
		},
 | 
			
		||||
		IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
 | 
			
		||||
			str := fmt.Sprintf("%v", val)
 | 
			
		||||
			if !IsValidUsername(str) {
 | 
			
		||||
				errs.Add([]string{name}, ErrUsername, "invalid username")
 | 
			
		||||
				return false, errs
 | 
			
		||||
			}
 | 
			
		||||
			return true, errs
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func portOnly(hostport string) string {
 | 
			
		||||
	colon := strings.IndexByte(hostport, ':')
 | 
			
		||||
	if colon == -1 {
 | 
			
		||||
 
 | 
			
		||||
@@ -91,3 +91,15 @@ func IsValidExternalTrackerURLFormat(uri string) bool {
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	validUsernamePattern   = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`)
 | 
			
		||||
	invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`) // No consecutive or trailing non-alphanumeric chars
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// IsValidUsername checks if username is valid
 | 
			
		||||
func IsValidUsername(name string) bool {
 | 
			
		||||
	// It is difficult to find a single pattern that is both readable and effective,
 | 
			
		||||
	// but it's easier to use positive and negative checks.
 | 
			
		||||
	return validUsernamePattern.MatchString(name) && !invalidUsernamePattern.MatchString(name)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -155,3 +155,34 @@ func Test_IsValidExternalTrackerURLFormat(t *testing.T) {
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestIsValidUsername(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		arg  string
 | 
			
		||||
		want bool
 | 
			
		||||
	}{
 | 
			
		||||
		{arg: "a", want: true},
 | 
			
		||||
		{arg: "abc", want: true},
 | 
			
		||||
		{arg: "0.b-c", want: true},
 | 
			
		||||
		{arg: "a.b-c_d", want: true},
 | 
			
		||||
		{arg: "", want: false},
 | 
			
		||||
		{arg: ".abc", want: false},
 | 
			
		||||
		{arg: "abc.", want: false},
 | 
			
		||||
		{arg: "a..bc", want: false},
 | 
			
		||||
		{arg: "a...bc", want: false},
 | 
			
		||||
		{arg: "a.-bc", want: false},
 | 
			
		||||
		{arg: "a._bc", want: false},
 | 
			
		||||
		{arg: "a_-bc", want: false},
 | 
			
		||||
		{arg: "a/bc", want: false},
 | 
			
		||||
		{arg: "☁️", want: false},
 | 
			
		||||
		{arg: "-", want: false},
 | 
			
		||||
		{arg: "--diff", want: false},
 | 
			
		||||
		{arg: "-im-here", want: false},
 | 
			
		||||
		{arg: "a space", want: false},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.arg, func(t *testing.T) {
 | 
			
		||||
			assert.Equalf(t, tt.want, IsValidUsername(tt.arg), "IsValidUsername(%v)", tt.arg)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -135,6 +135,8 @@ func Validate(errs binding.Errors, data map[string]interface{}, f Form, l transl
 | 
			
		||||
				data["ErrorMsg"] = trName + l.Tr("form.glob_pattern_error", errs[0].Message)
 | 
			
		||||
			case validation.ErrRegexPattern:
 | 
			
		||||
				data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
 | 
			
		||||
			case validation.ErrUsername:
 | 
			
		||||
				data["ErrorMsg"] = trName + l.Tr("form.username_error")
 | 
			
		||||
			default:
 | 
			
		||||
				msg := errs[0].Classification
 | 
			
		||||
				if msg != "" && errs[0].Message != "" {
 | 
			
		||||
 
 | 
			
		||||
@@ -463,6 +463,7 @@ url_error = `'%s' is not a valid URL.`
 | 
			
		||||
include_error = ` must contain substring '%s'.`
 | 
			
		||||
glob_pattern_error = ` glob pattern is invalid: %s.`
 | 
			
		||||
regex_pattern_error = ` regex pattern is invalid: %s.`
 | 
			
		||||
username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.`
 | 
			
		||||
unknown_error = Unknown error:
 | 
			
		||||
captcha_incorrect = The CAPTCHA code is incorrect.
 | 
			
		||||
password_not_match = The passwords do not match.
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ import (
 | 
			
		||||
type AdminCreateUserForm struct {
 | 
			
		||||
	LoginType          string `binding:"Required"`
 | 
			
		||||
	LoginName          string
 | 
			
		||||
	UserName           string `binding:"Required;AlphaDashDot;MaxSize(40)"`
 | 
			
		||||
	UserName           string `binding:"Required;Username;MaxSize(40)"`
 | 
			
		||||
	Email              string `binding:"Required;Email;MaxSize(254)"`
 | 
			
		||||
	Password           string `binding:"MaxSize(255)"`
 | 
			
		||||
	SendNotify         bool
 | 
			
		||||
@@ -35,7 +35,7 @@ func (f *AdminCreateUserForm) Validate(req *http.Request, errs binding.Errors) b
 | 
			
		||||
// AdminEditUserForm form for admin to create user
 | 
			
		||||
type AdminEditUserForm struct {
 | 
			
		||||
	LoginType               string `binding:"Required"`
 | 
			
		||||
	UserName                string `binding:"AlphaDashDot;MaxSize(40)"`
 | 
			
		||||
	UserName                string `binding:"Username;MaxSize(40)"`
 | 
			
		||||
	LoginName               string
 | 
			
		||||
	FullName                string `binding:"MaxSize(100)"`
 | 
			
		||||
	Email                   string `binding:"Required;Email;MaxSize(254)"`
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ import (
 | 
			
		||||
 | 
			
		||||
// CreateOrgForm form for creating organization
 | 
			
		||||
type CreateOrgForm struct {
 | 
			
		||||
	OrgName                   string `binding:"Required;AlphaDashDot;MaxSize(40)" locale:"org.org_name_holder"`
 | 
			
		||||
	OrgName                   string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"`
 | 
			
		||||
	Visibility                structs.VisibleType
 | 
			
		||||
	RepoAdminChangeTeamAccess bool
 | 
			
		||||
}
 | 
			
		||||
@@ -37,7 +37,7 @@ func (f *CreateOrgForm) Validate(req *http.Request, errs binding.Errors) binding
 | 
			
		||||
 | 
			
		||||
// UpdateOrgSettingForm form for updating organization settings
 | 
			
		||||
type UpdateOrgSettingForm struct {
 | 
			
		||||
	Name                      string `binding:"Required;AlphaDashDot;MaxSize(40)" locale:"org.org_name_holder"`
 | 
			
		||||
	Name                      string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"`
 | 
			
		||||
	FullName                  string `binding:"MaxSize(100)"`
 | 
			
		||||
	Description               string `binding:"MaxSize(255)"`
 | 
			
		||||
	Website                   string `binding:"ValidUrl;MaxSize(255)"`
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,7 @@ type InstallForm struct {
 | 
			
		||||
 | 
			
		||||
	PasswordAlgorithm string
 | 
			
		||||
 | 
			
		||||
	AdminName          string `binding:"OmitEmpty;AlphaDashDot;MaxSize(30)" locale:"install.admin_name"`
 | 
			
		||||
	AdminName          string `binding:"OmitEmpty;Username;MaxSize(30)" locale:"install.admin_name"`
 | 
			
		||||
	AdminPasswd        string `binding:"OmitEmpty;MaxSize(255)" locale:"install.admin_password"`
 | 
			
		||||
	AdminConfirmPasswd string
 | 
			
		||||
	AdminEmail         string `binding:"OmitEmpty;MinSize(3);MaxSize(254);Include(@)" locale:"install.admin_email"`
 | 
			
		||||
@@ -91,7 +91,7 @@ func (f *InstallForm) Validate(req *http.Request, errs binding.Errors) binding.E
 | 
			
		||||
 | 
			
		||||
// RegisterForm form for registering
 | 
			
		||||
type RegisterForm struct {
 | 
			
		||||
	UserName           string `binding:"Required;AlphaDashDot;MaxSize(40)"`
 | 
			
		||||
	UserName           string `binding:"Required;Username;MaxSize(40)"`
 | 
			
		||||
	Email              string `binding:"Required;MaxSize(254)"`
 | 
			
		||||
	Password           string `binding:"MaxSize(255)"`
 | 
			
		||||
	Retype             string
 | 
			
		||||
@@ -243,7 +243,7 @@ func (f *IntrospectTokenForm) Validate(req *http.Request, errs binding.Errors) b
 | 
			
		||||
 | 
			
		||||
// UpdateProfileForm form for updating profile
 | 
			
		||||
type UpdateProfileForm struct {
 | 
			
		||||
	Name                string `binding:"AlphaDashDot;MaxSize(40)"`
 | 
			
		||||
	Name                string `binding:"Username;MaxSize(40)"`
 | 
			
		||||
	FullName            string `binding:"MaxSize(100)"`
 | 
			
		||||
	KeepEmailPrivate    bool
 | 
			
		||||
	Website             string `binding:"ValidSiteUrl;MaxSize(255)"`
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ func (f *SignInOpenIDForm) Validate(req *http.Request, errs binding.Errors) bind
 | 
			
		||||
 | 
			
		||||
// SignUpOpenIDForm form for signin up with OpenID
 | 
			
		||||
type SignUpOpenIDForm struct {
 | 
			
		||||
	UserName           string `binding:"Required;AlphaDashDot;MaxSize(40)"`
 | 
			
		||||
	UserName           string `binding:"Required;Username;MaxSize(40)"`
 | 
			
		||||
	Email              string `binding:"Required;Email;MaxSize(254)"`
 | 
			
		||||
	GRecaptchaResponse string `form:"g-recaptcha-response"`
 | 
			
		||||
	HcaptchaResponse   string `form:"h-captcha-response"`
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,22 @@ func TestRenameInvalidUsername(t *testing.T) {
 | 
			
		||||
		"%00",
 | 
			
		||||
		"thisHas ASpace",
 | 
			
		||||
		"p<A>tho>lo<gical",
 | 
			
		||||
		".",
 | 
			
		||||
		"..",
 | 
			
		||||
		".well-known",
 | 
			
		||||
		".abc",
 | 
			
		||||
		"abc.",
 | 
			
		||||
		"a..bc",
 | 
			
		||||
		"a...bc",
 | 
			
		||||
		"a.-bc",
 | 
			
		||||
		"a._bc",
 | 
			
		||||
		"a_-bc",
 | 
			
		||||
		"a/bc",
 | 
			
		||||
		"☁️",
 | 
			
		||||
		"-",
 | 
			
		||||
		"--diff",
 | 
			
		||||
		"-im-here",
 | 
			
		||||
		"a space",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	session := loginUser(t, "user2")
 | 
			
		||||
@@ -68,7 +84,7 @@ func TestRenameInvalidUsername(t *testing.T) {
 | 
			
		||||
		htmlDoc := NewHTMLParser(t, resp.Body)
 | 
			
		||||
		assert.Contains(t,
 | 
			
		||||
			htmlDoc.doc.Find(".ui.negative.message").Text(),
 | 
			
		||||
			translation.NewLocale("en-US").Tr("form.alpha_dash_dot_error"),
 | 
			
		||||
			translation.NewLocale("en-US").Tr("form.username_error"),
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		unittest.AssertNotExistsBean(t, &user_model.User{Name: invalidUsername})
 | 
			
		||||
@@ -79,9 +95,7 @@ func TestRenameReservedUsername(t *testing.T) {
 | 
			
		||||
	defer tests.PrepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	reservedUsernames := []string{
 | 
			
		||||
		".",
 | 
			
		||||
		"..",
 | 
			
		||||
		".well-known",
 | 
			
		||||
		// ".", "..", ".well-known", // The names are not only reserved but also invalid
 | 
			
		||||
		"admin",
 | 
			
		||||
		"api",
 | 
			
		||||
		"assets",
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user