diff --git a/docs/node-configuration.md b/docs/node-configuration.md index d80ed5267..1c14bc76a 100644 --- a/docs/node-configuration.md +++ b/docs/node-configuration.md @@ -187,6 +187,8 @@ RPC: MaxFindResultItems: 100 MaxFindStoragePageSize: 50 MaxNEP11Tokens: 100 + MaxRequestBodyBytes: 5242880 + MaxRequestHeaderBytes: 1048576 MaxWebSocketClients: 64 SessionEnabled: false SessionExpirationTime: 15 @@ -225,6 +227,10 @@ where: - `MaxFindStoragePageSize` - the maximum number of elements for `findstorage` response per single page. - `MaxNEP11Tokens` - limit for the number of tokens returned from `getnep11balances` call. +- `MaxRequestBodyBytes` - the maximum allowed HTTP request body size in bytes + (5MB by default). +- `MaxRequestHeaderBytes` - the maximum allowed HTTP request header size in bytes + (1MB by default). - `MaxWebSocketClients` - the maximum simultaneous websocket client connection number (64 by default). Attempts to establish additional connections will lead to websocket handshake failures. Use "-1" to disable websocket diff --git a/pkg/config/config.go b/pkg/config/config.go index d30a30e61..cd2c157c3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,6 +3,7 @@ package config import ( "bytes" "fmt" + "net/http" "os" "time" @@ -30,6 +31,12 @@ const ( // DefaultMaxNEP11Tokens is the default maximum number of resulting NEP11 tokens // that can be traversed by `getnep11balances` JSON-RPC handler. DefaultMaxNEP11Tokens = 100 + // DefaultMaxRequestBodyBytes is the default maximum allowed size of HTTP + // request body in bytes. + DefaultMaxRequestBodyBytes = 5 * 1024 * 1024 + // DefaultMaxRequestHeaderBytes is the maximum permitted size of the headers + // in an HTTP request. + DefaultMaxRequestHeaderBytes = http.DefaultMaxHeaderBytes ) // Version is the version of the node, set at the build time. diff --git a/pkg/config/rpc_config.go b/pkg/config/rpc_config.go index bd9d0acdd..c9b0a1630 100644 --- a/pkg/config/rpc_config.go +++ b/pkg/config/rpc_config.go @@ -16,6 +16,8 @@ type ( MaxFindResultItems int `yaml:"MaxFindResultItems"` MaxFindStorageResultItems int `yaml:"MaxFindStoragePageSize"` MaxNEP11Tokens int `yaml:"MaxNEP11Tokens"` + MaxRequestBodyBytes int `yaml:"MaxRequestBodyBytes"` + MaxRequestHeaderBytes int `yaml:"MaxRequestHeaderBytes"` MaxWebSocketClients int `yaml:"MaxWebSocketClients"` SessionEnabled bool `yaml:"SessionEnabled"` SessionExpirationTime int `yaml:"SessionExpirationTime"` diff --git a/pkg/neorpc/types.go b/pkg/neorpc/types.go index 93e60e6a1..790d27341 100644 --- a/pkg/neorpc/types.go +++ b/pkg/neorpc/types.go @@ -115,6 +115,15 @@ func (s *SignerWithWitness) UnmarshalJSON(data []byte) error { if err != nil { return fmt.Errorf("not a signer: %w", err) } + if len(aux.AllowedContracts) > transaction.MaxAttributes { + return fmt.Errorf("invalid number of AllowedContracts: got %d, allowed %d at max", len(aux.AllowedContracts), transaction.MaxAttributes) + } + if len(aux.AllowedGroups) > transaction.MaxAttributes { + return fmt.Errorf("invalid number of AllowedGroups: got %d, allowed %d at max", len(aux.AllowedGroups), transaction.MaxAttributes) + } + if len(aux.Rules) > transaction.MaxAttributes { + return fmt.Errorf("invalid number of Rules: got %d, allowed %d at max", len(aux.Rules), transaction.MaxAttributes) + } acc, err := util.Uint160DecodeStringLE(strings.TrimPrefix(aux.Account, "0x")) if err != nil { acc, err = address.StringToUint160(aux.Account) diff --git a/pkg/neorpc/types_test.go b/pkg/neorpc/types_test.go index f0c99ea23..c817bc1e8 100644 --- a/pkg/neorpc/types_test.go +++ b/pkg/neorpc/types_test.go @@ -2,10 +2,12 @@ package neorpc import ( "encoding/json" + "fmt" "testing" "github.com/nspcc-dev/neo-go/internal/testserdes" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/stretchr/testify/require" ) @@ -37,4 +39,54 @@ func TestSignerWithWitnessMarshalUnmarshalJSON(t *testing.T) { actual, err := json.Marshal(s) require.NoError(t, err) require.Equal(t, expected, string(actual)) + + t.Run("subitems overflow", func(t *testing.T) { + checkSubitems := func(t *testing.T, bad any) { + data, err := json.Marshal(bad) + require.NoError(t, err) + err = json.Unmarshal(data, &SignerWithWitness{}) + + require.Error(t, err) + require.Contains(t, err.Error(), fmt.Sprintf("got %d, allowed %d at max", transaction.MaxAttributes+1, transaction.MaxAttributes)) + } + + t.Run("groups", func(t *testing.T) { + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + bad := &SignerWithWitness{ + Signer: transaction.Signer{ + AllowedGroups: make([]*keys.PublicKey, transaction.MaxAttributes+1), + }, + } + for i := range bad.AllowedGroups { + bad.AllowedGroups[i] = pk.PublicKey() + } + + checkSubitems(t, bad) + }) + t.Run("contracts", func(t *testing.T) { + bad := &SignerWithWitness{ + Signer: transaction.Signer{ + AllowedContracts: make([]util.Uint160, transaction.MaxAttributes+1), + }, + } + + checkSubitems(t, bad) + }) + t.Run("rules", func(t *testing.T) { + bad := &SignerWithWitness{ + Signer: transaction.Signer{ + Rules: make([]transaction.WitnessRule, transaction.MaxAttributes+1), + }, + } + for i := range bad.Rules { + bad.Rules[i] = transaction.WitnessRule{ + Action: transaction.WitnessAllow, + Condition: &transaction.ConditionScriptHash{}, + } + } + + checkSubitems(t, bad) + }) + }) } diff --git a/pkg/services/rpcsrv/params/param.go b/pkg/services/rpcsrv/params/param.go index 29d390648..3e07f22f4 100644 --- a/pkg/services/rpcsrv/params/param.go +++ b/pkg/services/rpcsrv/params/param.go @@ -394,7 +394,7 @@ func (p *Param) GetSignerWithWitness() (neorpc.SignerWithWitness, error) { // GetSignersWithWitnesses returns a slice of SignerWithWitness with CalledByEntry // scope from an array of Uint160 or an array of serialized transaction.Signer stored // in the parameter. -func (p Param) GetSignersWithWitnesses() ([]transaction.Signer, []transaction.Witness, error) { +func (p *Param) GetSignersWithWitnesses() ([]transaction.Signer, []transaction.Witness, error) { hashes, err := p.GetArray() if err != nil { return nil, nil, err diff --git a/pkg/services/rpcsrv/server.go b/pkg/services/rpcsrv/server.go index bb54fc4a3..6e7a6ff55 100644 --- a/pkg/services/rpcsrv/server.go +++ b/pkg/services/rpcsrv/server.go @@ -267,25 +267,6 @@ var rpcWsHandlers = map[string]func(*Server, params.Params, *subscriber) (any, * // untyped nil or non-nil structure implementing OracleHandler interface. func New(chain Ledger, conf config.RPC, coreServer *network.Server, orc OracleHandler, log *zap.Logger, errChan chan<- error) Server { - addrs := conf.Addresses - httpServers := make([]*http.Server, len(addrs)) - for i, addr := range addrs { - httpServers[i] = &http.Server{ - Addr: addr, - } - } - - var tlsServers []*http.Server - if cfg := conf.TLSConfig; cfg.Enabled { - addrs := cfg.Addresses - tlsServers = make([]*http.Server, len(addrs)) - for i, addr := range addrs { - tlsServers[i] = &http.Server{ - Addr: addr, - } - } - } - protoCfg := chain.GetConfig().ProtocolConfiguration if conf.SessionEnabled { if conf.SessionExpirationTime <= 0 { @@ -313,6 +294,14 @@ func New(chain Ledger, conf config.RPC, coreServer *network.Server, conf.MaxNEP11Tokens = config.DefaultMaxNEP11Tokens log.Info("MaxNEP11Tokens is not set or wrong, setting default value", zap.Int("MaxNEP11Tokens", config.DefaultMaxNEP11Tokens)) } + if conf.MaxRequestBodyBytes <= 0 { + conf.MaxRequestBodyBytes = config.DefaultMaxRequestBodyBytes + log.Info("MaxRequestBodyBytes is not set or wong, setting default value", zap.Int("MaxRequestBodyBytes", config.DefaultMaxRequestBodyBytes)) + } + if conf.MaxRequestHeaderBytes <= 0 { + conf.MaxRequestHeaderBytes = config.DefaultMaxRequestHeaderBytes + log.Info("MaxRequestHeaderBytes is not set or wong, setting default value", zap.Int("MaxRequestHeaderBytes", config.DefaultMaxRequestHeaderBytes)) + } if conf.MaxWebSocketClients == 0 { conf.MaxWebSocketClients = defaultMaxWebSocketClients log.Info("MaxWebSocketClients is not set or wrong, setting default value", zap.Int("MaxWebSocketClients", defaultMaxWebSocketClients)) @@ -325,6 +314,28 @@ func New(chain Ledger, conf config.RPC, coreServer *network.Server, if conf.EnableCORSWorkaround { wsOriginChecker = func(_ *http.Request) bool { return true } } + + addrs := conf.Addresses + httpServers := make([]*http.Server, len(addrs)) + for i, addr := range addrs { + httpServers[i] = &http.Server{ + Addr: addr, + MaxHeaderBytes: conf.MaxRequestHeaderBytes, + } + } + + var tlsServers []*http.Server + if cfg := conf.TLSConfig; cfg.Enabled { + addrs := cfg.Addresses + tlsServers = make([]*http.Server, len(addrs)) + for i, addr := range addrs { + tlsServers[i] = &http.Server{ + Addr: addr, + MaxHeaderBytes: conf.MaxRequestHeaderBytes, + } + } + } + return Server{ http: httpServers, https: tlsServers, @@ -474,6 +485,8 @@ func (s *Server) SetOracleHandler(orc OracleHandler) { } func (s *Server) handleHTTPRequest(w http.ResponseWriter, httpRequest *http.Request) { + // Restrict request body before further processing. + httpRequest.Body = http.MaxBytesReader(w, httpRequest.Body, int64(s.config.MaxRequestBodyBytes)) req := params.NewRequest() if httpRequest.URL.Path == "/ws" && httpRequest.Method == "GET" {