From 7b67aba87758fc02c6f2ed25413e73b28a6f0981 Mon Sep 17 00:00:00 2001 From: webb Date: Mon, 18 Sep 2023 03:31:38 -0400 Subject: [PATCH] Security: Add client limits for HTTP requests for time and response body. --- conf/bloat.conf | 9 ++++++ conf/conf.go | 63 ++++++++++++++++++++++++++++++++++++------ conf/const.go | 5 +++- service/client.go | 43 ++++++++++++++++++++++++++++ service/handlers.go | 2 +- service/service.go | 4 ++- service/transaction.go | 4 +-- 7 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 service/client.go diff --git a/conf/bloat.conf b/conf/bloat.conf index e1bfab4..294c193 100644 --- a/conf/bloat.conf +++ b/conf/bloat.conf @@ -49,3 +49,12 @@ snowflake_node_id=0 # This sets the user agent given to the instance. 8bloat will use a suitable user # agent by default, so in most situations you shouldn't touch this. # user_agent=8bloat/X.X.X (Floyd's Instance, https://8b.example.com) + +# This sets the maximum amount of data that can be read from an HTTP response +# from an upstream server to somewhere with memory. This doesn't limit the +# actual memory utilisation. In bytes. +# http_client_response_size_limit=10000000 + +# This sets the maximum time 8bloat will spend making a request to an upstream +# server. This defaults to eight seconds. +# http_client_timeout=30s \ No newline at end of file diff --git a/conf/conf.go b/conf/conf.go index c1c2e4c..994ed97 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -11,11 +11,13 @@ import ( "io" "io/fs" "log" + "net/http" "os" "runtime/debug" "strconv" "strings" "sync/atomic" + "time" "github.com/bwmarrin/snowflake" ) @@ -65,14 +67,16 @@ type PostFormat struct { } type Configuration struct { - ListenAddress string - ClientName string - ClientScope string - ClientWebsite string - PostFormats []PostFormat - AssetStamp string - UserAgent string - Instance string + ListenAddress string + ClientName string + ClientScope string + ClientWebsite string + PostFormats []PostFormat + AssetStamp string + UserAgent string + Instance string + ResponseLimit int64 + RequestTimeout time.Duration node int64 } @@ -157,6 +161,19 @@ func init() { } } +func init() { + http.DefaultTransport = &http.Transport{ + ExpectContinueTimeout: 1 * time.Second, + IdleConnTimeout: 3 * time.Second, + ForceAttemptHTTP2: true, + } + + http.DefaultClient = &http.Client{ + Transport: http.DefaultTransport, + Timeout: 8 * time.Second, + } +} + func readConf(reader io.Reader) error { var conf Configuration @@ -228,6 +245,28 @@ func readConf(reader io.Reader) error { return errors.New("invalid config key: " + val) } } + case "http_client_timeout": + if val != "" { + var err error + + conf.RequestTimeout, err = time.ParseDuration(val) + if err != nil { + return err + } + } + case "http_client_response_size_limit": + if val != "" { + i, err := strconv.Atoi(val) + if err != nil { + return errors.New("http_client_response_size_limit is not a number") + } + + if i < 0 { + return errors.New("http_client_response_size_limit cannot be negative") + } + + conf.ResponseLimit = int64(i) + } default: return errors.New("unknown config key " + key) } @@ -237,6 +276,14 @@ func readConf(reader io.Reader) error { conf.UserAgent = "8bloat/" + version + " (Mastodon client, https://spiderden.org/projects/8bloat)" } + if int64(conf.RequestTimeout) == 0 { + conf.RequestTimeout = time.Second * 8 + } + + if conf.ResponseLimit == 0 { + conf.ResponseLimit = (1 << (10 * 2)) * 8 // 8MB + } + currcfg := Get() if node == nil || (currcfg != nil && currcfg.node != conf.node) { nodelock.Lock() diff --git a/conf/const.go b/conf/const.go index 9402e88..bf67239 100644 --- a/conf/const.go +++ b/conf/const.go @@ -1,3 +1,6 @@ package conf -const MaxPagination = 20 +const ( + MaxPagination = 20 + MaxBodyResponse = 8000000 +) diff --git a/service/client.go b/service/client.go new file mode 100644 index 0000000..e7f3bde --- /dev/null +++ b/service/client.go @@ -0,0 +1,43 @@ +package service + +import ( + "net/http" + "time" + + "spiderden.org/8b/conf" + "spiderden.org/masta" +) + +type tripper struct { + underlying http.RoundTripper +} + +func (t *tripper) RoundTrip(r *http.Request) (*http.Response, error) { + response, err := t.underlying.RoundTrip(r) + if response != nil && response.Body != nil { + response.Body = http.MaxBytesReader(nil, response.Body, conf.Get().ResponseLimit) + } + + return response, err +} + +var client = http.Client{ + Transport: &tripper{ + underlying: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + }, + Timeout: 8 * time.Second, +} + +func newMastaClient(cfg *masta.Config) *masta.Client { + mclient := masta.NewClient(cfg) + mclient.Client = client + mclient.UserAgent = conf.Get().UserAgent + return mclient +} diff --git a/service/handlers.go b/service/handlers.go index 0472bad..eaae455 100644 --- a/service/handlers.go +++ b/service/handlers.go @@ -665,7 +665,7 @@ func handleOAuthCallback(t *Transaction) error { return errInvalidArgument } - t.Client = masta.NewClient(&masta.Config{ + t.Client = newMastaClient(&masta.Config{ Server: "https://" + t.Session.Instance, ClientID: t.Session.ClientID, ClientSecret: t.Session.ClientSecret, diff --git a/service/service.go b/service/service.go index cfd54a8..2e9ccd1 100644 --- a/service/service.go +++ b/service/service.go @@ -40,7 +40,9 @@ func StartAndListen(ctx context.Context) error { default: } - server = &http.Server{ + client.Timeout = conf.RequestTimeout + + server := &http.Server{ Addr: conf.ListenAddress, Handler: router, } diff --git a/service/transaction.go b/service/transaction.go index c13e56e..6178916 100644 --- a/service/transaction.go +++ b/service/transaction.go @@ -113,14 +113,13 @@ func (t *Transaction) authenticate(am authMode) (err error) { } t.Session = sess - t.Client = masta.NewClient(&masta.Config{ + t.Client = newMastaClient(&masta.Config{ Server: "https://" + t.Session.Instance, ClientID: t.Session.ClientID, ClientSecret: t.Session.ClientSecret, AccessToken: t.Session.AccessToken, }) - t.Client.UserAgent = conf.Get().UserAgent if am != authSessCSRF { return } @@ -151,6 +150,7 @@ func newSession(t *Transaction, instance string) (rurl string, sess *Session, er } app, err := masta.RegisterApp(t.Ctx, &masta.AppConfig{ + Client: client, Server: instanceURL, ClientName: t.Conf.ClientName, Scopes: t.Conf.ClientScope, -- 2.45.2