Restrict email address validation (#17688)
This didn't follow the RFC but it's a subset of that. I think we should narrow the allowed chars at first and discuss more possibility in future PRs.
This commit is contained in:
parent
49db87a035
commit
18033f49ba
9 changed files with 110 additions and 11 deletions
|
@ -10,6 +10,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
@ -22,7 +23,22 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrEmailNotActivated e-mail address has not been activated error
|
// ErrEmailNotActivated e-mail address has not been activated error
|
||||||
var ErrEmailNotActivated = errors.New("E-mail address has not been activated")
|
var ErrEmailNotActivated = errors.New("e-mail address has not been activated")
|
||||||
|
|
||||||
|
// ErrEmailCharIsNotSupported e-mail address contains unsupported character
|
||||||
|
type ErrEmailCharIsNotSupported struct {
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrEmailCharIsNotSupported checks if an error is an ErrEmailCharIsNotSupported
|
||||||
|
func IsErrEmailCharIsNotSupported(err error) bool {
|
||||||
|
_, ok := err.(ErrEmailCharIsNotSupported)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrEmailCharIsNotSupported) Error() string {
|
||||||
|
return fmt.Sprintf("e-mail address contains unsupported character [email: %s]", err.Email)
|
||||||
|
}
|
||||||
|
|
||||||
// ErrEmailInvalid represents an error where the email address does not comply with RFC 5322
|
// ErrEmailInvalid represents an error where the email address does not comply with RFC 5322
|
||||||
type ErrEmailInvalid struct {
|
type ErrEmailInvalid struct {
|
||||||
|
@ -106,12 +122,24 @@ func (email *EmailAddress) BeforeInsert() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||||
|
|
||||||
// ValidateEmail check if email is a allowed address
|
// ValidateEmail check if email is a allowed address
|
||||||
func ValidateEmail(email string) error {
|
func ValidateEmail(email string) error {
|
||||||
if len(email) == 0 {
|
if len(email) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !emailRegexp.MatchString(email) {
|
||||||
|
return ErrEmailCharIsNotSupported{email}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(email[0] >= 'a' && email[0] <= 'z') &&
|
||||||
|
!(email[0] >= 'A' && email[0] <= 'Z') &&
|
||||||
|
!(email[0] >= '0' && email[0] <= '9') {
|
||||||
|
return ErrEmailInvalid{email}
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := mail.ParseAddress(email); err != nil {
|
if _, err := mail.ParseAddress(email); err != nil {
|
||||||
return ErrEmailInvalid{email}
|
return ErrEmailInvalid{email}
|
||||||
}
|
}
|
||||||
|
|
|
@ -252,3 +252,58 @@ func TestListEmails(t *testing.T) {
|
||||||
assert.Len(t, emails, 5)
|
assert.Len(t, emails, 5)
|
||||||
assert.Greater(t, count, int64(len(emails)))
|
assert.Greater(t, count, int64(len(emails)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEmailAddressValidate(t *testing.T) {
|
||||||
|
kases := map[string]error{
|
||||||
|
"abc@gmail.com": nil,
|
||||||
|
"132@hotmail.com": nil,
|
||||||
|
"1-3-2@test.org": nil,
|
||||||
|
"1.3.2@test.org": nil,
|
||||||
|
"a_123@test.org.cn": nil,
|
||||||
|
`first.last@iana.org`: nil,
|
||||||
|
`first!last@iana.org`: nil,
|
||||||
|
`first#last@iana.org`: nil,
|
||||||
|
`first$last@iana.org`: nil,
|
||||||
|
`first%last@iana.org`: nil,
|
||||||
|
`first&last@iana.org`: nil,
|
||||||
|
`first'last@iana.org`: nil,
|
||||||
|
`first*last@iana.org`: nil,
|
||||||
|
`first+last@iana.org`: nil,
|
||||||
|
`first/last@iana.org`: nil,
|
||||||
|
`first=last@iana.org`: nil,
|
||||||
|
`first?last@iana.org`: nil,
|
||||||
|
`first^last@iana.org`: nil,
|
||||||
|
"first`last@iana.org": nil,
|
||||||
|
`first{last@iana.org`: nil,
|
||||||
|
`first|last@iana.org`: nil,
|
||||||
|
`first}last@iana.org`: nil,
|
||||||
|
`first~last@iana.org`: nil,
|
||||||
|
`first;last@iana.org`: ErrEmailCharIsNotSupported{`first;last@iana.org`},
|
||||||
|
".233@qq.com": ErrEmailInvalid{".233@qq.com"},
|
||||||
|
"!233@qq.com": ErrEmailInvalid{"!233@qq.com"},
|
||||||
|
"#233@qq.com": ErrEmailInvalid{"#233@qq.com"},
|
||||||
|
"$233@qq.com": ErrEmailInvalid{"$233@qq.com"},
|
||||||
|
"%233@qq.com": ErrEmailInvalid{"%233@qq.com"},
|
||||||
|
"&233@qq.com": ErrEmailInvalid{"&233@qq.com"},
|
||||||
|
"'233@qq.com": ErrEmailInvalid{"'233@qq.com"},
|
||||||
|
"*233@qq.com": ErrEmailInvalid{"*233@qq.com"},
|
||||||
|
"+233@qq.com": ErrEmailInvalid{"+233@qq.com"},
|
||||||
|
"/233@qq.com": ErrEmailInvalid{"/233@qq.com"},
|
||||||
|
"=233@qq.com": ErrEmailInvalid{"=233@qq.com"},
|
||||||
|
"?233@qq.com": ErrEmailInvalid{"?233@qq.com"},
|
||||||
|
"^233@qq.com": ErrEmailInvalid{"^233@qq.com"},
|
||||||
|
"`233@qq.com": ErrEmailInvalid{"`233@qq.com"},
|
||||||
|
"{233@qq.com": ErrEmailInvalid{"{233@qq.com"},
|
||||||
|
"|233@qq.com": ErrEmailInvalid{"|233@qq.com"},
|
||||||
|
"}233@qq.com": ErrEmailInvalid{"}233@qq.com"},
|
||||||
|
"~233@qq.com": ErrEmailInvalid{"~233@qq.com"},
|
||||||
|
";233@qq.com": ErrEmailCharIsNotSupported{";233@qq.com"},
|
||||||
|
"Foo <foo@bar.com>": ErrEmailCharIsNotSupported{"Foo <foo@bar.com>"},
|
||||||
|
string([]byte{0xE2, 0x84, 0xAA}): ErrEmailCharIsNotSupported{string([]byte{0xE2, 0x84, 0xAA})},
|
||||||
|
}
|
||||||
|
for kase, err := range kases {
|
||||||
|
t.Run(kase, func(t *testing.T) {
|
||||||
|
assert.EqualValues(t, err, ValidateEmail(kase))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -644,6 +644,15 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e
|
||||||
u.Visibility = overwriteDefault[0].Visibility
|
u.Visibility = overwriteDefault[0].Visibility
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate data
|
||||||
|
if err := validateUser(u); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateEmail(u.Email); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
ctx, committer, err := db.TxContext()
|
ctx, committer, err := db.TxContext()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -652,11 +661,6 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e
|
||||||
|
|
||||||
sess := db.GetEngine(ctx)
|
sess := db.GetEngine(ctx)
|
||||||
|
|
||||||
// validate data
|
|
||||||
if err := validateUser(u); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
isExist, err := isUserExist(sess, 0, u.Name)
|
isExist, err := isUserExist(sess, 0, u.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -232,7 +232,7 @@ func TestCreateUserInvalidEmail(t *testing.T) {
|
||||||
|
|
||||||
err := CreateUser(user)
|
err := CreateUser(user)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.True(t, IsErrEmailInvalid(err))
|
assert.True(t, IsErrEmailCharIsNotSupported(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateUserEmailAlreadyUsed(t *testing.T) {
|
func TestCreateUserEmailAlreadyUsed(t *testing.T) {
|
||||||
|
|
|
@ -119,6 +119,7 @@ func CreateUser(ctx *context.APIContext) {
|
||||||
user_model.IsErrEmailAlreadyUsed(err) ||
|
user_model.IsErrEmailAlreadyUsed(err) ||
|
||||||
db.IsErrNameReserved(err) ||
|
db.IsErrNameReserved(err) ||
|
||||||
db.IsErrNameCharsNotAllowed(err) ||
|
db.IsErrNameCharsNotAllowed(err) ||
|
||||||
|
user_model.IsErrEmailCharIsNotSupported(err) ||
|
||||||
user_model.IsErrEmailInvalid(err) ||
|
user_model.IsErrEmailInvalid(err) ||
|
||||||
db.IsErrNamePatternNotAllowed(err) {
|
db.IsErrNamePatternNotAllowed(err) {
|
||||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||||
|
@ -265,7 +266,9 @@ func EditUser(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := user_model.UpdateUser(u, emailChanged); err != nil {
|
if err := user_model.UpdateUser(u, emailChanged); err != nil {
|
||||||
if user_model.IsErrEmailAlreadyUsed(err) || user_model.IsErrEmailInvalid(err) {
|
if user_model.IsErrEmailAlreadyUsed(err) ||
|
||||||
|
user_model.IsErrEmailCharIsNotSupported(err) ||
|
||||||
|
user_model.IsErrEmailInvalid(err) {
|
||||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||||
} else {
|
} else {
|
||||||
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
|
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
|
||||||
|
|
|
@ -80,7 +80,8 @@ func AddEmail(ctx *context.APIContext) {
|
||||||
if err := user_model.AddEmailAddresses(emails); err != nil {
|
if err := user_model.AddEmailAddresses(emails); err != nil {
|
||||||
if user_model.IsErrEmailAlreadyUsed(err) {
|
if user_model.IsErrEmailAlreadyUsed(err) {
|
||||||
ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email)
|
ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email)
|
||||||
} else if user_model.IsErrEmailInvalid(err) {
|
} else if user_model.IsErrEmailCharIsNotSupported(err) ||
|
||||||
|
user_model.IsErrEmailInvalid(err) {
|
||||||
errMsg := fmt.Sprintf("Email address %s invalid", err.(user_model.ErrEmailInvalid).Email)
|
errMsg := fmt.Sprintf("Email address %s invalid", err.(user_model.ErrEmailInvalid).Email)
|
||||||
ctx.Error(http.StatusUnprocessableEntity, "", errMsg)
|
ctx.Error(http.StatusUnprocessableEntity, "", errMsg)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -171,6 +171,9 @@ func NewUserPost(ctx *context.Context) {
|
||||||
case user_model.IsErrEmailAlreadyUsed(err):
|
case user_model.IsErrEmailAlreadyUsed(err):
|
||||||
ctx.Data["Err_Email"] = true
|
ctx.Data["Err_Email"] = true
|
||||||
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form)
|
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form)
|
||||||
|
case user_model.IsErrEmailCharIsNotSupported(err):
|
||||||
|
ctx.Data["Err_Email"] = true
|
||||||
|
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
|
||||||
case user_model.IsErrEmailInvalid(err):
|
case user_model.IsErrEmailInvalid(err):
|
||||||
ctx.Data["Err_Email"] = true
|
ctx.Data["Err_Email"] = true
|
||||||
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
|
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
|
||||||
|
@ -386,7 +389,8 @@ func EditUserPost(ctx *context.Context) {
|
||||||
if user_model.IsErrEmailAlreadyUsed(err) {
|
if user_model.IsErrEmailAlreadyUsed(err) {
|
||||||
ctx.Data["Err_Email"] = true
|
ctx.Data["Err_Email"] = true
|
||||||
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
|
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
|
||||||
} else if user_model.IsErrEmailInvalid(err) {
|
} else if user_model.IsErrEmailCharIsNotSupported(err) ||
|
||||||
|
user_model.IsErrEmailInvalid(err) {
|
||||||
ctx.Data["Err_Email"] = true
|
ctx.Data["Err_Email"] = true
|
||||||
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
|
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -573,6 +573,9 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form interface{
|
||||||
case user_model.IsErrEmailAlreadyUsed(err):
|
case user_model.IsErrEmailAlreadyUsed(err):
|
||||||
ctx.Data["Err_Email"] = true
|
ctx.Data["Err_Email"] = true
|
||||||
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form)
|
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form)
|
||||||
|
case user_model.IsErrEmailCharIsNotSupported(err):
|
||||||
|
ctx.Data["Err_Email"] = true
|
||||||
|
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
|
||||||
case user_model.IsErrEmailInvalid(err):
|
case user_model.IsErrEmailInvalid(err):
|
||||||
ctx.Data["Err_Email"] = true
|
ctx.Data["Err_Email"] = true
|
||||||
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
|
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
|
||||||
|
|
|
@ -188,7 +188,8 @@ func EmailPost(ctx *context.Context) {
|
||||||
|
|
||||||
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
|
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
|
||||||
return
|
return
|
||||||
} else if user_model.IsErrEmailInvalid(err) {
|
} else if user_model.IsErrEmailCharIsNotSupported(err) ||
|
||||||
|
user_model.IsErrEmailInvalid(err) {
|
||||||
loadAccountData(ctx)
|
loadAccountData(ctx)
|
||||||
|
|
||||||
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form)
|
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form)
|
||||||
|
|
Reference in a new issue