From f85102d1fbe0aacbef38379408bec7e3a63ca38b Mon Sep 17 00:00:00 2001 From: tamirms Date: Wed, 13 Sep 2023 09:25:27 +0100 Subject: [PATCH] services/horizon/internal: Add flag to limit request body size (#16) --- services/horizon/internal/app.go | 1 + services/horizon/internal/config.go | 2 + services/horizon/internal/flags.go | 12 +++- services/horizon/internal/httpx/router.go | 6 ++ .../internal/integration/txsub_test.go | 60 ++++++++++++++++++- 5 files changed, 79 insertions(+), 2 deletions(-) diff --git a/services/horizon/internal/app.go b/services/horizon/internal/app.go index 809657eeb6..635b0b9359 100644 --- a/services/horizon/internal/app.go +++ b/services/horizon/internal/app.go @@ -521,6 +521,7 @@ func (a *App) init() error { SSEUpdateFrequency: a.config.SSEUpdateFrequency, StaleThreshold: a.config.StaleThreshold, ConnectionTimeout: a.config.ConnectionTimeout, + MaxHTTPRequestSize: a.config.MaxHTTPRequestSize, NetworkPassphrase: a.config.NetworkPassphrase, MaxPathLength: a.config.MaxPathLength, MaxAssetsPerPathRequest: a.config.MaxAssetsPerPathRequest, diff --git a/services/horizon/internal/config.go b/services/horizon/internal/config.go index f1961bea1c..fb6853ea72 100644 --- a/services/horizon/internal/config.go +++ b/services/horizon/internal/config.go @@ -40,6 +40,8 @@ type Config struct { SSEUpdateFrequency time.Duration ConnectionTimeout time.Duration + // MaxHTTPRequestSize is the maximum allowed request payload size + MaxHTTPRequestSize uint RateQuota *throttled.RateQuota FriendbotURL *url.URL LogLevel logrus.Level diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index 34d9314758..d2cc055ef8 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -12,6 +12,8 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/viper" + "github.com/stellar/throttled" + "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/network" "github.com/stellar/go/services/horizon/internal/db2/schema" @@ -20,7 +22,6 @@ import ( "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" "github.com/stellar/go/support/log" - "github.com/stellar/throttled" ) const ( @@ -62,6 +63,8 @@ const ( StellarPubnet = "pubnet" // StellarTestnet is a constant representing the Stellar test network StellarTestnet = "testnet" + + defaultMaxHTTPRequestSize = uint(200 * 1024) ) // validateBothOrNeither ensures that both options are provided, if either is provided. @@ -366,6 +369,13 @@ func Flags() (*Config, support.ConfigOptions) { CustomSetValue: support.SetDuration, Usage: "defines the timeout of connection after which 504 response will be sent or stream will be closed, if Horizon is behind a load balancer with idle connection timeout, this should be set to a few seconds less that idle timeout, does not apply to POST /transactions", }, + &support.ConfigOption{ + Name: "max-http-request-size", + ConfigKey: &config.MaxHTTPRequestSize, + OptType: types.Uint, + FlagDefault: defaultMaxHTTPRequestSize, + Usage: "sets the limit on the maximum allowed http request payload size, default is 200kb, to disable the limit check, set to 0, only do so if you acknowledge the implications of accepting unbounded http request payload sizes.", + }, &support.ConfigOption{ Name: "per-hour-rate-limit", ConfigKey: &config.RateQuota, diff --git a/services/horizon/internal/httpx/router.go b/services/horizon/internal/httpx/router.go index c9ca733876..c04eb14a87 100644 --- a/services/horizon/internal/httpx/router.go +++ b/services/horizon/internal/httpx/router.go @@ -38,6 +38,7 @@ type RouterConfig struct { SSEUpdateFrequency time.Duration StaleThreshold uint ConnectionTimeout time.Duration + MaxHTTPRequestSize uint NetworkPassphrase string MaxPathLength uint MaxAssetsPerPathRequest int @@ -89,6 +90,11 @@ func (r *Router) addMiddleware(config *RouterConfig, })) r.Use(loggerMiddleware(serverMetrics)) r.Use(timeoutMiddleware(config.ConnectionTimeout)) + if config.MaxHTTPRequestSize > 0 { + r.Use(func(handler http.Handler) http.Handler { + return http.MaxBytesHandler(handler, int64(config.MaxHTTPRequestSize)) + }) + } r.Use(recoverMiddleware) r.Use(chimiddleware.Compress(flate.DefaultCompression, "application/hal+json")) diff --git a/services/horizon/internal/integration/txsub_test.go b/services/horizon/internal/integration/txsub_test.go index 23c9a54def..1ee6018d1f 100644 --- a/services/horizon/internal/integration/txsub_test.go +++ b/services/horizon/internal/integration/txsub_test.go @@ -1,11 +1,16 @@ package integration import ( + "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/services/horizon/internal/test/integration" "github.com/stellar/go/txnbuild" - "github.com/stretchr/testify/assert" + "github.com/stellar/go/xdr" ) func TestTxSub(t *testing.T) { @@ -50,3 +55,56 @@ func TestTxSub(t *testing.T) { assert.Error(t, err) }) } + +func TestTxSubLimitsBodySize(t *testing.T) { + if integration.GetCoreMaxSupportedProtocol() < 20 { + t.Skip("This test run does not support less than Protocol 20") + } + + itest := integration.NewTest(t, integration.Config{ + ProtocolVersion: 20, + EnableSorobanRPC: true, + HorizonEnvironment: map[string]string{ + "MAX_HTTP_REQUEST_SIZE": "5000", + }, + }) + + // establish which account will be contract owner, and load it's current seq + sourceAccount, err := itest.Client().AccountDetail(horizonclient.AccountRequest{ + AccountID: itest.Master().Address(), + }) + require.NoError(t, err) + + contract := []byte(strings.Repeat("a", 10*1024)) + installContractOp := &txnbuild.InvokeHostFunction{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeUploadContractWasm, + Wasm: &contract, + }, + SourceAccount: itest.Master().Address(), + } + preFlightOp, minFee := itest.PreflightHostFunctions(&sourceAccount, *installContractOp) + _, err = itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp) + assert.EqualError( + t, err, + "horizon error: \"Transaction Malformed\" - check horizon.Error.Problem for more information", + ) + + sourceAccount, err = itest.Client().AccountDetail(horizonclient.AccountRequest{ + AccountID: itest.Master().Address(), + }) + require.NoError(t, err) + + contract = []byte(strings.Repeat("a", 2*1024)) + installContractOp = &txnbuild.InvokeHostFunction{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeUploadContractWasm, + Wasm: &contract, + }, + SourceAccount: itest.Master().Address(), + } + preFlightOp, minFee = itest.PreflightHostFunctions(&sourceAccount, *installContractOp) + tx, err := itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp) + require.NoError(t, err) + require.True(t, tx.Successful) +}