Skip to content

Commit 97e9ec8

Browse files
committed
fix: add rate limiter for email endpoints
1 parent d0982b3 commit 97e9ec8

File tree

2 files changed

+48
-8
lines changed

2 files changed

+48
-8
lines changed

api/api.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,12 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
114114

115115
r.Get("/authorize", api.ExternalProviderRedirect)
116116

117-
r.With(api.requireAdminCredentials).Post("/invite", api.Invite)
117+
r.With(api.limitEmailSentHandler()).With(api.requireAdminCredentials).Post("/invite", api.Invite)
118+
r.With(api.limitEmailSentHandler()).With(api.verifyCaptcha).Post("/signup", api.Signup)
119+
r.With(api.limitEmailSentHandler()).With(api.verifyCaptcha).With(api.requireEmailProvider).Post("/recover", api.Recover)
120+
r.With(api.limitEmailSentHandler()).With(api.verifyCaptcha).Post("/magiclink", api.MagicLink)
118121

119-
r.With(api.verifyCaptcha).Post("/signup", api.Signup)
120-
r.With(api.verifyCaptcha).With(api.requireEmailProvider).Post("/recover", api.Recover)
121-
r.With(api.verifyCaptcha).Post("/magiclink", api.MagicLink)
122-
r.With(api.verifyCaptcha).Post("/otp", api.Otp)
122+
r.With(api.limitEmailSentHandler()).With(api.verifyCaptcha).Post("/otp", api.Otp)
123123

124124
r.With(api.requireEmailProvider).With(api.limitHandler(
125125
// Allow requests at a rate of 30 per 5 minutes.
@@ -143,7 +143,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
143143
r.Route("/user", func(r *router) {
144144
r.Use(api.requireAuthentication)
145145
r.Get("/", api.UserGet)
146-
r.Put("/", api.UserUpdate)
146+
r.With(api.limitEmailSentHandler()).Put("/", api.UserUpdate)
147147
})
148148

149149
r.Route("/admin", func(r *router) {

api/middleware.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import (
44
"bytes"
55
"context"
66
"encoding/json"
7-
"github.com/netlify/gotrue/security"
8-
"github.com/sirupsen/logrus"
97
"io"
108
"io/ioutil"
119
"net/http"
1210
"strings"
11+
"time"
12+
13+
"github.com/netlify/gotrue/security"
14+
"github.com/sirupsen/logrus"
1315

1416
"github.com/didip/tollbooth/v5"
1517
"github.com/didip/tollbooth/v5/limiter"
@@ -154,6 +156,44 @@ func (a *API) limitHandler(lmt *limiter.Limiter) middlewareHandler {
154156
}
155157
}
156158

159+
func (a *API) limitEmailSentHandler() middlewareHandler {
160+
// limit per hour
161+
freq := a.config.RateLimitEmailSent / (60 * 60)
162+
lmt := tollbooth.NewLimiter(freq, &limiter.ExpirableOptions{
163+
DefaultExpirationTTL: time.Hour,
164+
}).SetBurst(int(a.config.RateLimitEmailSent)).SetMethods([]string{"PUT", "POST"})
165+
return func(w http.ResponseWriter, req *http.Request) (context.Context, error) {
166+
c := req.Context()
167+
config := a.getConfig(c)
168+
if config.External.Email.Enabled && !config.Mailer.Autoconfirm {
169+
if req.Method == "PUT" || req.Method == "POST" {
170+
res := make(map[string]interface{})
171+
bodyBytes, err := ioutil.ReadAll(req.Body)
172+
if err != nil {
173+
return c, internalServerError("Error invalid request body").WithInternalError(err)
174+
}
175+
req.Body.Close()
176+
req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
177+
178+
jsonDecoder := json.NewDecoder(bytes.NewBuffer(bodyBytes))
179+
if err := jsonDecoder.Decode(&res); err != nil {
180+
return c, badRequestError("Error invalid request body").WithInternalError(err)
181+
}
182+
183+
if _, ok := res["email"]; !ok {
184+
// email not in POST body
185+
return c, nil
186+
}
187+
188+
if err := tollbooth.LimitByRequest(lmt, w, req); err != nil {
189+
return c, httpError(http.StatusTooManyRequests, "Rate limit exceeded")
190+
}
191+
}
192+
}
193+
return c, nil
194+
}
195+
}
196+
157197
func (a *API) requireAdminCredentials(w http.ResponseWriter, req *http.Request) (context.Context, error) {
158198
ctx := req.Context()
159199
t, err := a.extractBearerToken(w, req)

0 commit comments

Comments
 (0)