webapi: Rate limit for admin login requests.

Only allow 3 requests per second. Return "429 Too Many Requests" when this rate is exceeded.
This commit is contained in:
jholdstock 2023-03-02 12:40:44 +00:00 committed by Jamie Holdstock
parent d00ba70f94
commit 62803147f0
6 changed files with 59 additions and 9 deletions

1
go.mod
View File

@ -59,6 +59,7 @@ require (
golang.org/x/net v0.6.0 // indirect golang.org/x/net v0.6.0 // indirect
golang.org/x/sys v0.5.0 // indirect golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect golang.org/x/text v0.7.0 // indirect
golang.org/x/time v0.3.0
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )

2
go.sum
View File

@ -242,6 +242,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2022 The Decred developers // Copyright (c) 2020-2023 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -202,7 +202,7 @@ func (s *Server) adminLogin(c *gin.Context) {
c.HTML(http.StatusUnauthorized, "login.html", gin.H{ c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"WebApiCache": s.cache.getData(), "WebApiCache": s.cache.getData(),
"WebApiCfg": s.cfg, "WebApiCfg": s.cfg,
"IncorrectPassword": true, "FailedLoginMsg": "Incorrect password",
}) })
return return
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2022 The Decred developers // Copyright (c) 2020-2023 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -10,6 +10,8 @@ import (
"io" "io"
"net/http" "net/http"
"strings" "strings"
"sync"
"time"
"github.com/decred/dcrd/blockchain/stake/v4" "github.com/decred/dcrd/blockchain/stake/v4"
"github.com/decred/vspd/rpc" "github.com/decred/vspd/rpc"
@ -18,8 +20,43 @@ import (
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/jrick/wsrpc/v2" "github.com/jrick/wsrpc/v2"
"golang.org/x/time/rate"
) )
// rateLimit middleware limits how many requests each client IP can submit per
// second. If the limit is exceeded the limitExceeded handler will be executed
// and the context will be aborted.
func rateLimit(limit rate.Limit, limitExceeded gin.HandlerFunc) gin.HandlerFunc {
var limitersMtx sync.Mutex
limiters := make(map[string]*rate.Limiter)
// Clear limiters every hour so they arent accumulating infinitely.
go func() {
for {
<-time.After(time.Hour)
limitersMtx.Lock()
limiters = make(map[string]*rate.Limiter)
limitersMtx.Unlock()
}
}()
return func(c *gin.Context) {
limitersMtx.Lock()
defer limitersMtx.Unlock()
// Create a limiter for this IP if one does not exist.
if _, ok := limiters[c.ClientIP()]; !ok {
limiters[c.ClientIP()] = rate.NewLimiter(limit, 1)
}
// Check if this IP exceeds limit.
if !limiters[c.ClientIP()].Allow() {
limitExceeded(c)
c.Abort()
}
}
}
// withSession middleware adds a gorilla session to the request context for // withSession middleware adds a gorilla session to the request context for
// downstream handlers to make use of. Sessions are used by admin pages to // downstream handlers to make use of. Sessions are used by admin pages to
// maintain authentication status. // maintain authentication status.

View File

@ -4,9 +4,9 @@
<h1>Login</h1> <h1>Login</h1>
<form class="py-3" action="/admin" method="post"> <form class="py-3" action="/admin" method="post">
<input type="password" name="password" required placeholder="Enter password"> <input type="password" name="password" autofocus required placeholder="Enter password">
<p class="my-1 orange" style="visibility:{{ if .IncorrectPassword }}visible{{ else }}hidden{{ end }};">Incorrect password</p> <p class="my-1 orange" style="visibility:{{ if .FailedLoginMsg }}visible{{ else }}hidden{{ end }};">{{ .FailedLoginMsg }}</p>
<button class="btn btn-primary" type="submit">Login</button> <button class="btn btn-primary" type="submit">Login</button>
</form> </form>

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2022 The Decred developers // Copyright (c) 2020-2023 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -249,7 +249,17 @@ func (s *Server) router(cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.W
login := router.Group("/admin").Use( login := router.Group("/admin").Use(
s.withSession(cookieStore), s.withSession(cookieStore),
) )
login.POST("", s.adminLogin)
// Limit login attempts to 3 per second.
loginRateLmiter := rateLimit(3, func(c *gin.Context) {
s.log.Warnf("Login rate limit exceeded by %s", c.ClientIP())
c.HTML(http.StatusTooManyRequests, "login.html", gin.H{
"WebApiCache": s.cache.getData(),
"WebApiCfg": s.cfg,
"FailedLoginMsg": "Rate limit exceeded",
})
})
login.POST("", loginRateLmiter, s.adminLogin)
admin := router.Group("/admin").Use( admin := router.Group("/admin").Use(
s.withWalletClients(wallets), s.withSession(cookieStore), s.requireAdmin, s.withWalletClients(wallets), s.withSession(cookieStore), s.requireAdmin,