Skip to content

Commit c0c5b23

Browse files
authored
fix: enforce authorized address checks on send email only (#1806)
Moves the authorized address only check in the flow that sends the email, instead of in the email address validation flow.
1 parent 9354b83 commit c0c5b23

File tree

4 files changed

+52
-106
lines changed

4 files changed

+52
-106
lines changed

internal/api/api.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne
134134

135135
r.Route("/", func(r *router) {
136136
r.Use(api.isValidExternalHost)
137-
r.Use(api.isValidAuthorizedEmail)
138137

139138
r.Get("/settings", api.Settings)
140139

internal/api/mail.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package api
22

33
import (
44
"net/http"
5+
"regexp"
56
"strings"
67
"time"
78

@@ -23,7 +24,6 @@ import (
2324

2425
var (
2526
EmailRateLimitExceeded error = errors.New("email rate limit exceeded")
26-
AddressNotAuthorized error = errors.New("Destination email address not authorized")
2727
)
2828

2929
type GenerateLinkParams struct {
@@ -320,6 +320,8 @@ func (a *API) sendConfirmation(r *http.Request, tx *storage.Connection, u *model
320320
u.ConfirmationToken = oldToken
321321
if errors.Is(err, EmailRateLimitExceeded) {
322322
return tooManyRequestsError(ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error())
323+
} else if herr, ok := err.(*HTTPError); ok {
324+
return herr
323325
}
324326
return internalServerError("Error sending confirmation email").WithInternalError(err)
325327
}
@@ -351,6 +353,8 @@ func (a *API) sendInvite(r *http.Request, tx *storage.Connection, u *models.User
351353
u.ConfirmationToken = oldToken
352354
if errors.Is(err, EmailRateLimitExceeded) {
353355
return tooManyRequestsError(ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error())
356+
} else if herr, ok := err.(*HTTPError); ok {
357+
return herr
354358
}
355359
return internalServerError("Error sending invite email").WithInternalError(err)
356360
}
@@ -390,6 +394,8 @@ func (a *API) sendPasswordRecovery(r *http.Request, tx *storage.Connection, u *m
390394
u.RecoveryToken = oldToken
391395
if errors.Is(err, EmailRateLimitExceeded) {
392396
return tooManyRequestsError(ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error())
397+
} else if herr, ok := err.(*HTTPError); ok {
398+
return herr
393399
}
394400
return internalServerError("Error sending recovery email").WithInternalError(err)
395401
}
@@ -428,6 +434,8 @@ func (a *API) sendReauthenticationOtp(r *http.Request, tx *storage.Connection, u
428434
u.ReauthenticationToken = oldToken
429435
if errors.Is(err, EmailRateLimitExceeded) {
430436
return tooManyRequestsError(ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error())
437+
} else if herr, ok := err.(*HTTPError); ok {
438+
return herr
431439
}
432440
return internalServerError("Error sending reauthentication email").WithInternalError(err)
433441
}
@@ -467,6 +475,8 @@ func (a *API) sendMagicLink(r *http.Request, tx *storage.Connection, u *models.U
467475
u.RecoveryToken = oldToken
468476
if errors.Is(err, EmailRateLimitExceeded) {
469477
return tooManyRequestsError(ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error())
478+
} else if herr, ok := err.(*HTTPError); ok {
479+
return herr
470480
}
471481
return internalServerError("Error sending magic link email").WithInternalError(err)
472482
}
@@ -517,6 +527,8 @@ func (a *API) sendEmailChange(r *http.Request, tx *storage.Connection, u *models
517527
if err := a.sendEmail(r, tx, u, mail.EmailChangeVerification, otpCurrent, otpNew, u.EmailChangeTokenNew); err != nil {
518528
if errors.Is(err, EmailRateLimitExceeded) {
519529
return tooManyRequestsError(ErrorCodeOverEmailSendRateLimit, EmailRateLimitExceeded.Error())
530+
} else if herr, ok := err.(*HTTPError); ok {
531+
return herr
520532
}
521533
return internalServerError("Error sending email change email").WithInternalError(err)
522534
}
@@ -569,13 +581,52 @@ func validateSentWithinFrequencyLimit(sentAt *time.Time, frequency time.Duration
569581
return nil
570582
}
571583

584+
var emailLabelPattern = regexp.MustCompile("[+][^@]+@")
585+
586+
func (a *API) checkEmailAddressAuthorization(email string) bool {
587+
if len(a.config.External.Email.AuthorizedAddresses) > 0 {
588+
// allow labelled emails when authorization rules are in place
589+
normalized := emailLabelPattern.ReplaceAllString(email, "@")
590+
591+
for _, authorizedAddress := range a.config.External.Email.AuthorizedAddresses {
592+
if strings.EqualFold(normalized, authorizedAddress) {
593+
return true
594+
}
595+
}
596+
597+
return false
598+
}
599+
600+
return true
601+
}
602+
572603
func (a *API) sendEmail(r *http.Request, tx *storage.Connection, u *models.User, emailActionType, otp, otpNew, tokenHashWithPrefix string) error {
573604
mailer := a.Mailer()
574605
ctx := r.Context()
575606
config := a.config
576607
referrerURL := utilities.GetReferrer(r, config)
577608
externalURL := getExternalHost(ctx)
578609

610+
if emailActionType != mail.EmailChangeVerification {
611+
if u.GetEmail() != "" && !a.checkEmailAddressAuthorization(u.GetEmail()) {
612+
return badRequestError(ErrorCodeEmailAddressNotAuthorized, "Email address %q cannot be used as it is not authorized", u.GetEmail())
613+
}
614+
} else {
615+
// first check that the user can update their address to the
616+
// new one in u.EmailChange
617+
if u.EmailChange != "" && !a.checkEmailAddressAuthorization(u.EmailChange) {
618+
return badRequestError(ErrorCodeEmailAddressNotAuthorized, "Email address %q cannot be used as it is not authorized", u.EmailChange)
619+
}
620+
621+
// if secure email change is enabled, check that the user
622+
// account (which could have been created before the authorized
623+
// address authorization restriction was enabled) can even
624+
// receive the confirmation message to the existing address
625+
if config.Mailer.SecureEmailChangeEnabled && u.GetEmail() != "" && !a.checkEmailAddressAuthorization(u.GetEmail()) {
626+
return badRequestError(ErrorCodeEmailAddressNotAuthorized, "Email address %q cannot be used as it is not authorized", u.GetEmail())
627+
}
628+
}
629+
579630
// apply rate limiting before the email is sent out
580631
if ok := a.limiterOpts.Email.Allow(); !ok {
581632
emailRateLimitCounter.Add(

internal/api/middleware.go

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"fmt"
88
"net/http"
99
"net/url"
10-
"regexp"
1110
"strings"
1211
"sync"
1312
"time"
@@ -138,46 +137,6 @@ func isIgnoreCaptchaRoute(req *http.Request) bool {
138137
return false
139138
}
140139

141-
var emailLabelPattern = regexp.MustCompile("[+][^@]+@")
142-
143-
// we don't need to enforce the check on these endpoints since they don't send emails
144-
var containsNonEmailSendingPath = regexp.MustCompile(`^/(admin|token|verify)`)
145-
146-
func (a *API) isValidAuthorizedEmail(w http.ResponseWriter, req *http.Request) (context.Context, error) {
147-
ctx := req.Context()
148-
149-
// skip checking for authorized email addresses if it's an admin request
150-
if containsNonEmailSendingPath.MatchString(req.URL.Path) || req.Method == http.MethodGet || req.Method == http.MethodDelete {
151-
return ctx, nil
152-
}
153-
154-
var body struct {
155-
Email string `json:"email"`
156-
}
157-
158-
if err := retrieveRequestParams(req, &body); err != nil {
159-
// let downstream handlers handle the error
160-
return ctx, nil
161-
}
162-
if body.Email == "" {
163-
return ctx, nil
164-
}
165-
email := strings.ToLower(body.Email)
166-
if len(a.config.External.Email.AuthorizedAddresses) > 0 {
167-
// allow labelled emails when authorization rules are in place
168-
normalized := emailLabelPattern.ReplaceAllString(email, "@")
169-
170-
for _, authorizedAddress := range a.config.External.Email.AuthorizedAddresses {
171-
if normalized == authorizedAddress {
172-
return ctx, nil
173-
}
174-
}
175-
176-
return ctx, badRequestError(ErrorCodeEmailAddressNotAuthorized, "Email address %q cannot be used as it is not authorized", email)
177-
}
178-
return ctx, nil
179-
}
180-
181140
func (a *API) isValidExternalHost(w http.ResponseWriter, req *http.Request) (context.Context, error) {
182141
ctx := req.Context()
183142
config := a.config

internal/api/middleware_test.go

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -341,66 +341,3 @@ func (ts *MiddlewareTestSuite) TestLimitHandler() {
341341
ts.API.limitHandler(lmt).handler(okHandler).ServeHTTP(w, req)
342342
require.Equal(ts.T(), http.StatusTooManyRequests, w.Code)
343343
}
344-
345-
func (ts *MiddlewareTestSuite) TestIsValidAuthorizedEmail() {
346-
ts.API.config.External.Email.AuthorizedAddresses = []string{"[email protected]"}
347-
348-
cases := []struct {
349-
desc string
350-
reqPath string
351-
body map[string]interface{}
352-
}{
353-
{
354-
desc: "bypass check for admin endpoints",
355-
reqPath: "/admin",
356-
body: map[string]interface{}{
357-
"email": "[email protected]",
358-
},
359-
},
360-
{
361-
desc: "bypass check for token endpoint",
362-
reqPath: "/token",
363-
body: map[string]interface{}{
364-
"email": "[email protected]",
365-
},
366-
},
367-
{
368-
desc: "bypass check for verify endpoint",
369-
reqPath: "/verify",
370-
body: map[string]interface{}{
371-
"email": "[email protected]",
372-
},
373-
},
374-
{
375-
desc: "bypass check if no email in request body",
376-
reqPath: "/signup",
377-
body: map[string]interface{}{},
378-
},
379-
{
380-
desc: "email not in authorized list",
381-
reqPath: "/signup",
382-
body: map[string]interface{}{
383-
"email": "[email protected]",
384-
},
385-
},
386-
{
387-
desc: "email in authorized list",
388-
reqPath: "/signup",
389-
body: map[string]interface{}{
390-
"email": "[email protected]",
391-
},
392-
},
393-
}
394-
395-
for _, c := range cases {
396-
ts.Run(c.desc, func() {
397-
var buffer bytes.Buffer
398-
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.body))
399-
req := httptest.NewRequest(http.MethodPost, "http://localhost"+c.reqPath, &buffer)
400-
w := httptest.NewRecorder()
401-
if _, err := ts.API.isValidAuthorizedEmail(w, req); err != nil {
402-
require.Equal(ts.T(), err.(*HTTPError).ErrorCode, ErrorCodeEmailAddressNotAuthorized)
403-
}
404-
})
405-
}
406-
}

0 commit comments

Comments
 (0)