Merge pull request #3221 from nspcc-dev/json-limits

Anti-DOS RPC limitations
This commit is contained in:
Roman Khimov 2023-11-23 23:02:50 +03:00 committed by GitHub
commit 61d5b6eb0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 109 additions and 20 deletions

View file

@ -187,6 +187,8 @@ RPC:
MaxFindResultItems: 100 MaxFindResultItems: 100
MaxFindStoragePageSize: 50 MaxFindStoragePageSize: 50
MaxNEP11Tokens: 100 MaxNEP11Tokens: 100
MaxRequestBodyBytes: 5242880
MaxRequestHeaderBytes: 1048576
MaxWebSocketClients: 64 MaxWebSocketClients: 64
SessionEnabled: false SessionEnabled: false
SessionExpirationTime: 15 SessionExpirationTime: 15
@ -225,6 +227,10 @@ where:
- `MaxFindStoragePageSize` - the maximum number of elements for `findstorage` response per single page. - `MaxFindStoragePageSize` - the maximum number of elements for `findstorage` response per single page.
- `MaxNEP11Tokens` - limit for the number of tokens returned from - `MaxNEP11Tokens` - limit for the number of tokens returned from
`getnep11balances` call. `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 - `MaxWebSocketClients` - the maximum simultaneous websocket client connection
number (64 by default). Attempts to establish additional connections will number (64 by default). Attempts to establish additional connections will
lead to websocket handshake failures. Use "-1" to disable websocket lead to websocket handshake failures. Use "-1" to disable websocket

View file

@ -3,6 +3,7 @@ package config
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"net/http"
"os" "os"
"time" "time"
@ -30,6 +31,12 @@ const (
// DefaultMaxNEP11Tokens is the default maximum number of resulting NEP11 tokens // DefaultMaxNEP11Tokens is the default maximum number of resulting NEP11 tokens
// that can be traversed by `getnep11balances` JSON-RPC handler. // that can be traversed by `getnep11balances` JSON-RPC handler.
DefaultMaxNEP11Tokens = 100 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. // Version is the version of the node, set at the build time.

View file

@ -16,6 +16,8 @@ type (
MaxFindResultItems int `yaml:"MaxFindResultItems"` MaxFindResultItems int `yaml:"MaxFindResultItems"`
MaxFindStorageResultItems int `yaml:"MaxFindStoragePageSize"` MaxFindStorageResultItems int `yaml:"MaxFindStoragePageSize"`
MaxNEP11Tokens int `yaml:"MaxNEP11Tokens"` MaxNEP11Tokens int `yaml:"MaxNEP11Tokens"`
MaxRequestBodyBytes int `yaml:"MaxRequestBodyBytes"`
MaxRequestHeaderBytes int `yaml:"MaxRequestHeaderBytes"`
MaxWebSocketClients int `yaml:"MaxWebSocketClients"` MaxWebSocketClients int `yaml:"MaxWebSocketClients"`
SessionEnabled bool `yaml:"SessionEnabled"` SessionEnabled bool `yaml:"SessionEnabled"`
SessionExpirationTime int `yaml:"SessionExpirationTime"` SessionExpirationTime int `yaml:"SessionExpirationTime"`

View file

@ -115,6 +115,15 @@ func (s *SignerWithWitness) UnmarshalJSON(data []byte) error {
if err != nil { if err != nil {
return fmt.Errorf("not a signer: %w", err) 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")) acc, err := util.Uint160DecodeStringLE(strings.TrimPrefix(aux.Account, "0x"))
if err != nil { if err != nil {
acc, err = address.StringToUint160(aux.Account) acc, err = address.StringToUint160(aux.Account)

View file

@ -2,10 +2,12 @@ package neorpc
import ( import (
"encoding/json" "encoding/json"
"fmt"
"testing" "testing"
"github.com/nspcc-dev/neo-go/internal/testserdes" "github.com/nspcc-dev/neo-go/internal/testserdes"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "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/nspcc-dev/neo-go/pkg/util"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -37,4 +39,54 @@ func TestSignerWithWitnessMarshalUnmarshalJSON(t *testing.T) {
actual, err := json.Marshal(s) actual, err := json.Marshal(s)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, string(actual)) 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)
})
})
} }

View file

@ -394,7 +394,7 @@ func (p *Param) GetSignerWithWitness() (neorpc.SignerWithWitness, error) {
// GetSignersWithWitnesses returns a slice of SignerWithWitness with CalledByEntry // GetSignersWithWitnesses returns a slice of SignerWithWitness with CalledByEntry
// scope from an array of Uint160 or an array of serialized transaction.Signer stored // scope from an array of Uint160 or an array of serialized transaction.Signer stored
// in the parameter. // 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() hashes, err := p.GetArray()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

View file

@ -267,25 +267,6 @@ var rpcWsHandlers = map[string]func(*Server, params.Params, *subscriber) (any, *
// untyped nil or non-nil structure implementing OracleHandler interface. // untyped nil or non-nil structure implementing OracleHandler interface.
func New(chain Ledger, conf config.RPC, coreServer *network.Server, func New(chain Ledger, conf config.RPC, coreServer *network.Server,
orc OracleHandler, log *zap.Logger, errChan chan<- error) 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 protoCfg := chain.GetConfig().ProtocolConfiguration
if conf.SessionEnabled { if conf.SessionEnabled {
if conf.SessionExpirationTime <= 0 { if conf.SessionExpirationTime <= 0 {
@ -313,6 +294,14 @@ func New(chain Ledger, conf config.RPC, coreServer *network.Server,
conf.MaxNEP11Tokens = config.DefaultMaxNEP11Tokens conf.MaxNEP11Tokens = config.DefaultMaxNEP11Tokens
log.Info("MaxNEP11Tokens is not set or wrong, setting default value", zap.Int("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 { if conf.MaxWebSocketClients == 0 {
conf.MaxWebSocketClients = defaultMaxWebSocketClients conf.MaxWebSocketClients = defaultMaxWebSocketClients
log.Info("MaxWebSocketClients is not set or wrong, setting default value", zap.Int("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 { if conf.EnableCORSWorkaround {
wsOriginChecker = func(_ *http.Request) bool { return true } 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{ return Server{
http: httpServers, http: httpServers,
https: tlsServers, https: tlsServers,
@ -474,6 +485,8 @@ func (s *Server) SetOracleHandler(orc OracleHandler) {
} }
func (s *Server) handleHTTPRequest(w http.ResponseWriter, httpRequest *http.Request) { 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() req := params.NewRequest()
if httpRequest.URL.Path == "/ws" && httpRequest.Method == "GET" { if httpRequest.URL.Path == "/ws" && httpRequest.Method == "GET" {