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
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

View file

@ -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.

View file

@ -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"`

View file

@ -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)

View file

@ -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)
})
})
}

View file

@ -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

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.
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" {