fix: email comments are removed from email addresses (#9074)

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9074
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2025-08-30 13:15:30 +02:00
commit 1b13fda06b
9 changed files with 99 additions and 21 deletions

View file

@ -72,16 +72,23 @@ func validateEmailBasic(email string) error {
} }
func validateEmailDomain(email string) error { func validateEmailDomain(email string) error {
if !IsEmailDomainAllowed(email) { if _, ok := IsEmailDomainAllowed(email); !ok {
return ErrEmailInvalid{email} return ErrEmailInvalid{email}
} }
return nil return nil
} }
func IsEmailDomainAllowed(email string) bool { func IsEmailDomainAllowed(email string) (validEmail, ok bool) {
return isEmailDomainAllowedInternal( // Normalized the address. This strips for example comments which could be
email, // used to smuggle a different domain
parsedAddress, err := mail.ParseAddress(email)
if err != nil {
return false, false
}
return true, isEmailDomainAllowedInternal(
parsedAddress.Address,
setting.Service.EmailDomainAllowList, setting.Service.EmailDomainAllowList,
setting.Service.EmailDomainBlockList) setting.Service.EmailDomainBlockList)
} }

View file

@ -67,8 +67,3 @@ func TestEmailAddressValidate(t *testing.T) {
}) })
} }
} }
func TestEmailDomainAllowList(t *testing.T) {
res := IsEmailDomainAllowed("someuser@localhost.localdomain")
assert.True(t, res)
}

View file

@ -149,7 +149,7 @@ func CreateUser(ctx *context.APIContext) {
return return
} }
if !validation.IsEmailDomainAllowed(u.Email) { if _, ok := validation.IsEmailDomainAllowed(u.Email); !ok {
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", u.Email)) ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", u.Email))
} }
@ -235,7 +235,7 @@ func EditUser(ctx *context.APIContext) {
return return
} }
if !validation.IsEmailDomainAllowed(*form.Email) { if _, ok := validation.IsEmailDomainAllowed(*form.Email); !ok {
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email)) ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email))
} }
} }

View file

@ -201,7 +201,7 @@ func NewUserPost(ctx *context.Context) {
return return
} }
if !validation.IsEmailDomainAllowed(u.Email) { if _, ok := validation.IsEmailDomainAllowed(u.Email); !ok {
ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", u.Email)) ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", u.Email))
} }
@ -421,7 +421,7 @@ func EditUserPost(ctx *context.Context) {
} }
return return
} }
if !validation.IsEmailDomainAllowed(form.Email) { if _, ok := validation.IsEmailDomainAllowed(form.Email); !ok {
ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", form.Email)) ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", form.Email))
} }
} }

View file

@ -453,7 +453,10 @@ func SignUpPost(ctx *context.Context) {
return return
} }
if !form.IsEmailDomainAllowed() { if emailValid, ok := form.IsEmailDomainAllowed(); !emailValid {
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSignUp, form)
return
} else if !ok {
ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form) ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form)
return return
} }

View file

@ -226,7 +226,10 @@ func LinkAccountPostRegister(ctx *context.Context) {
} }
} }
if !form.IsEmailDomainAllowed() { if emailValid, ok := form.IsEmailDomainAllowed(); !emailValid {
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSignUp, form)
return
} else if !ok {
ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form) ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form)
return return
} }

View file

@ -109,7 +109,7 @@ func (f *RegisterForm) Validate(req *http.Request, errs binding.Errors) binding.
// The email is marked as allowed if it matches any of the // The email is marked as allowed if it matches any of the
// domains in the whitelist or if it doesn't match any of // domains in the whitelist or if it doesn't match any of
// domains in the blocklist, if any such list is not empty. // domains in the blocklist, if any such list is not empty.
func (f *RegisterForm) IsEmailDomainAllowed() bool { func (f *RegisterForm) IsEmailDomainAllowed() (validEmail, ok bool) {
return validation.IsEmailDomainAllowed(f.Email) return validation.IsEmailDomainAllowed(f.Email)
} }

View file

@ -20,7 +20,9 @@ func TestRegisterForm_IsDomainAllowed_Empty(t *testing.T) {
form := RegisterForm{} form := RegisterForm{}
assert.True(t, form.IsEmailDomainAllowed()) emailValid, ok := form.IsEmailDomainAllowed()
assert.False(t, emailValid)
assert.False(t, ok)
} }
func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) { func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) {
@ -36,7 +38,8 @@ func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) {
for _, v := range tt { for _, v := range tt {
form := RegisterForm{Email: v.email} form := RegisterForm{Email: v.email}
assert.False(t, form.IsEmailDomainAllowed()) _, ok := form.IsEmailDomainAllowed()
assert.False(t, ok)
} }
} }
@ -59,7 +62,8 @@ func TestRegisterForm_IsDomainAllowed_AllowedEmail(t *testing.T) {
for _, v := range tt { for _, v := range tt {
form := RegisterForm{Email: v.email} form := RegisterForm{Email: v.email}
assert.Equal(t, v.valid, form.IsEmailDomainAllowed()) _, ok := form.IsEmailDomainAllowed()
assert.Equal(t, v.valid, ok)
} }
} }
@ -72,7 +76,6 @@ func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) {
}{ }{
{"security@gitea.io", false}, {"security@gitea.io", false},
{"security@gitea.example", true}, {"security@gitea.example", true},
{"invalid", true},
{"user@my.block", false}, {"user@my.block", false},
{"user@my.block1", true}, {"user@my.block1", true},
@ -81,7 +84,8 @@ func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) {
for _, v := range tt { for _, v := range tt {
form := RegisterForm{Email: v.email} form := RegisterForm{Email: v.email}
assert.Equal(t, v.valid, form.IsEmailDomainAllowed()) _, ok := form.IsEmailDomainAllowed()
assert.Equal(t, v.valid, ok)
} }
} }

View file

@ -0,0 +1,66 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"testing"
"forgejo.org/modules/setting"
"forgejo.org/modules/test"
"forgejo.org/modules/validation"
"forgejo.org/tests"
"github.com/gobwas/glob"
"github.com/stretchr/testify/assert"
)
func TestEmailBlocklist(t *testing.T) {
defer test.MockVariableValue(
&setting.Service.EmailDomainBlockList,
[]glob.Glob{glob.MustCompile("evil")},
)()
defer tests.PrepareTestEnv(t)()
emailValid, ok := validation.IsEmailDomainAllowed("🐸@pond")
assert.True(t, emailValid)
assert.True(t, ok)
emailValid, ok = validation.IsEmailDomainAllowed("🐸@pond (what-is-this@evil)")
assert.True(t, emailValid)
assert.True(t, ok)
emailValid, ok = validation.IsEmailDomainAllowed("jomo@evil")
assert.True(t, emailValid)
assert.False(t, ok)
emailValid, ok = validation.IsEmailDomainAllowed("jomo@evil (but-does-it@break)")
assert.True(t, emailValid)
assert.False(t, ok)
}
func TestEmailAllowlist(t *testing.T) {
defer test.MockVariableValue(
&setting.Service.EmailDomainAllowList,
[]glob.Glob{glob.MustCompile("pond")},
)()
defer tests.PrepareTestEnv(t)()
emailValid, ok := validation.IsEmailDomainAllowed("🐸@pond")
assert.True(t, emailValid)
assert.True(t, ok)
emailValid, ok = validation.IsEmailDomainAllowed("🐸@pond (what-is-this@evil)")
assert.True(t, emailValid)
assert.True(t, ok)
emailValid, ok = validation.IsEmailDomainAllowed("jomo@evil")
assert.True(t, emailValid)
assert.False(t, ok)
emailValid, ok = validation.IsEmailDomainAllowed("jomo@evil (but-does-it@break)")
assert.True(t, emailValid)
assert.False(t, ok)
}