diff --git a/consensus/XDPoS/utils/utils.go b/consensus/XDPoS/utils/utils.go index 6af811ad153c..624071f68af1 100644 --- a/consensus/XDPoS/utils/utils.go +++ b/consensus/XDPoS/utils/utils.go @@ -76,8 +76,9 @@ func DecodeBytesExtraFields(b []byte, val interface{}) error { if len(b) == 0 { return errors.New("extra field is 0 length") } - // Prevent payload attack, limit the size of extra field to 20k bytes. Normal Extrafield payload is less than 7k bytes. - if len(b) > 20000 { + // Prevent payload attack, limit the size of extra field to 20k bytes. + // Normal Extrafield payload is less than 7k bytes. + if len(b) > 20*1024 { return errors.New("extra field is too long") } diff --git a/core/types/block.go b/core/types/block.go index 4fad656260ea..63e5d661dd94 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -153,6 +153,15 @@ func (h *Header) HashNoValidator() common.Hash { var headerSize = common.StorageSize(reflect.TypeOf(Header{}).Size()) +const maxHeaderByteFieldSize = 20 * 1024 + +func checkHeaderByteFieldSize(name string, size int) error { + if size <= maxHeaderByteFieldSize { + return nil + } + return fmt.Errorf("too large block %s: size %d exceeds limit %d", name, size, maxHeaderByteFieldSize) +} + // Size returns the approximate memory used by all internal contents. It is used // to approximate and limit the memory consumption of various caches. func (h *Header) Size() common.StorageSize { @@ -163,6 +172,53 @@ func (h *Header) Size() common.StorageSize { return headerSize + common.StorageSize(len(h.Extra)+len(h.Validators)+len(h.Validator)+len(h.Penalties)+(h.Difficulty.BitLen()+h.Number.BitLen()+baseFeeBits)/8) } +// SanityCheck checks a few basic things -- these checks are way beyond what +// any 'sane' production values should hold, and can mainly be used to prevent +// that the unbounded fields are stuffed with junk data to add processing +// overhead +func (h *Header) SanityCheck() error { + if h.Number != nil && !h.Number.IsUint64() { + return fmt.Errorf("too large block number: bitlen %d", h.Number.BitLen()) + } + if h.Difficulty != nil { + if diffLen := h.Difficulty.BitLen(); diffLen > 80 { + return fmt.Errorf("too large block difficulty: bitlen %d", diffLen) + } + } + for _, field := range []struct { + name string + size int + }{ + {name: "extradata", size: len(h.Extra)}, + {name: "validators", size: len(h.Validators)}, + {name: "validator", size: len(h.Validator)}, + {name: "penalties", size: len(h.Penalties)}, + } { + if err := checkHeaderByteFieldSize(field.name, field.size); err != nil { + return err + } + } + if h.BaseFee != nil { + if bfLen := h.BaseFee.BitLen(); bfLen > 256 { + return fmt.Errorf("too large base fee: bitlen %d", bfLen) + } + } + return nil +} + +// SanityCheckHeaders verifies that a list of headers contains only reasonable values. +func SanityCheckHeaders(headers []*Header) error { + for index, header := range headers { + if header == nil { + return fmt.Errorf("nil block header at index %d", index) + } + if err := header.SanityCheck(); err != nil { + return err + } + } + return nil +} + // EmptyBody returns true if there is no additional 'body' to complete the header // that is: no transactions and no uncles. func (h *Header) EmptyBody() bool { @@ -425,6 +481,15 @@ func (b *Block) Size() uint64 { return uint64(c) } +// SanityCheck can be used to prevent that unbounded fields are +// stuffed with junk data to add processing overhead +func (b *Block) SanityCheck() error { + if err := b.header.SanityCheck(); err != nil { + return err + } + return SanityCheckHeaders(b.uncles) +} + type writeCounter uint64 func (c *writeCounter) Write(b []byte) (int, error) { diff --git a/core/types/block_test.go b/core/types/block_test.go index 2afeef96c081..fcda5cf83e1b 100644 --- a/core/types/block_test.go +++ b/core/types/block_test.go @@ -134,6 +134,98 @@ func TestUncleHash(t *testing.T) { } } +func TestHeaderSanityCheckExtraSize(t *testing.T) { + tests := []struct { + name string + extra []byte + wantErr bool + }{ + {name: "limit", extra: bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize)}, + {name: "over limit", extra: bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1), wantErr: true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + header := &Header{ + Difficulty: big.NewInt(1), + Number: big.NewInt(1), + Extra: test.extra, + } + err := header.SanityCheck() + if test.wantErr && err == nil { + t.Fatal("expected oversized extra data to fail sanity check") + } + if !test.wantErr && err != nil { + t.Fatalf("expected extra data at limit to pass sanity check: %v", err) + } + }) + } +} + +func TestHeaderSanityCheckXDPoSFieldSizes(t *testing.T) { + tests := []struct { + name string + set func(*Header) + }{ + { + name: "validators", + set: func(header *Header) { + header.Validators = bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1) + }, + }, + { + name: "validator", + set: func(header *Header) { + header.Validator = bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1) + }, + }, + { + name: "penalties", + set: func(header *Header) { + header.Penalties = bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1) + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + header := &Header{ + Difficulty: big.NewInt(1), + Number: big.NewInt(1), + } + test.set(header) + if err := header.SanityCheck(); err == nil { + t.Fatalf("expected oversized %s data to fail sanity check", test.name) + } + }) + } +} + +func TestBlockSanityCheckUncleExtraSize(t *testing.T) { + block := NewBlockWithHeader(&Header{ + Difficulty: big.NewInt(1), + Number: big.NewInt(1), + }).WithBody(Body{ + Uncles: []*Header{{ + Difficulty: big.NewInt(1), + Number: big.NewInt(1), + Extra: bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1), + }}, + }) + + if err := block.SanityCheck(); err == nil { + t.Fatal("expected oversized uncle extra data to fail sanity check") + } +} + +func TestSanityCheckHeadersExtraSize(t *testing.T) { + headers := []*Header{{ + Extra: bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1), + }} + + if err := SanityCheckHeaders(headers); err == nil { + t.Fatal("expected oversized header extra data to fail sanity check") + } +} + var benchBuffer = bytes.NewBuffer(make([]byte, 0, 32000)) func BenchmarkEncodeBlock(b *testing.B) { diff --git a/eth/fetcher/fetcher.go b/eth/fetcher/fetcher.go index 174d5eb9d9f7..1dd6afd3e143 100644 --- a/eth/fetcher/fetcher.go +++ b/eth/fetcher/fetcher.go @@ -767,7 +767,7 @@ func (f *Fetcher) forgetHash(hash common.Hash) { // Remove any pending fetches and decrement the DOS counters if announce := f.fetching[hash]; announce != nil { f.announces[announce.origin]-- - if f.announces[announce.origin] == 0 { + if f.announces[announce.origin] <= 0 { delete(f.announces, announce.origin) } delete(f.fetching, hash) @@ -776,7 +776,7 @@ func (f *Fetcher) forgetHash(hash common.Hash) { // Remove any pending completion requests and decrement the DOS counters for _, announce := range f.fetched[hash] { f.announces[announce.origin]-- - if f.announces[announce.origin] == 0 { + if f.announces[announce.origin] <= 0 { delete(f.announces, announce.origin) } } @@ -785,7 +785,7 @@ func (f *Fetcher) forgetHash(hash common.Hash) { // Remove any pending completions and decrement the DOS counters if announce := f.completing[hash]; announce != nil { f.announces[announce.origin]-- - if f.announces[announce.origin] == 0 { + if f.announces[announce.origin] <= 0 { delete(f.announces, announce.origin) } delete(f.completing, hash) diff --git a/eth/handler.go b/eth/handler.go index 6ce236a7d9b2..f5c2ef6a1348 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -497,6 +497,9 @@ func (pm *ProtocolManager) handleMsg(p *peer) error { if err := msg.Decode(&headers); err != nil { return errResp(ErrDecode, "msg %v: %v", msg, err) } + if err := types.SanityCheckHeaders(headers); err != nil { + return err + } // If no headers were received, but we're expending a DAO fork check, maybe it's that if len(headers) == 0 && p.forkDrop != nil { // Possibly an empty reply to the fork header checks, sanity check TDs @@ -577,6 +580,9 @@ func (pm *ProtocolManager) handleMsg(p *peer) error { if err := msg.Decode(&request); err != nil { return errResp(ErrDecode, "msg %v: %v", msg, err) } + if err := request.sanityCheck(); err != nil { + return err + } // Deliver them all to the downloader for queuing trasactions := make([][]*types.Transaction, len(request)) uncles := make([][]*types.Header, len(request)) @@ -715,6 +721,9 @@ func (pm *ProtocolManager) handleMsg(p *peer) error { if err := msg.Decode(&request); err != nil { return errResp(ErrDecode, "%v: %v", msg, err) } + if err := request.sanityCheck(); err != nil { + return err + } request.Block.ReceivedAt = msg.ReceivedAt request.Block.ReceivedFrom = p diff --git a/eth/protocol.go b/eth/protocol.go index ca3583647e22..18de1bd71665 100644 --- a/eth/protocol.go +++ b/eth/protocol.go @@ -208,6 +208,20 @@ type newBlockData struct { TD *big.Int } +// sanityCheck verifies that the values are reasonable, as a DoS protection +func (request *newBlockData) sanityCheck() error { + if err := request.Block.SanityCheck(); err != nil { + return err + } + // TD is 34 bits at mainnet block 105048538 + // TD is 32 bits at testnet block 84385810. + // If it becomes 100 million times larger, it will still fit within 100 bits. + if tdlen := request.TD.BitLen(); tdlen > 100 { + return fmt.Errorf("too large block TD: bitlen %d", tdlen) + } + return nil +} + // blockBody represents the data content of a single block. type blockBody struct { Transactions []*types.Transaction // Transactions contained within a block @@ -216,3 +230,16 @@ type blockBody struct { // blockBodiesData is the network packet for block content distribution. type blockBodiesData []*blockBody + +// sanityCheck verifies that the uncle headers are reasonable, as a DoS protection. +func (request blockBodiesData) sanityCheck() error { + for bodyIndex, body := range request { + if body == nil { + return fmt.Errorf("nil block body at index %d", bodyIndex) + } + if err := types.SanityCheckHeaders(body.Uncles); err != nil { + return err + } + } + return nil +} diff --git a/eth/protocol_test.go b/eth/protocol_test.go index c85815150c2e..1fd00039ee5b 100644 --- a/eth/protocol_test.go +++ b/eth/protocol_test.go @@ -17,6 +17,7 @@ package eth import ( + "bytes" "fmt" "sync" "testing" @@ -180,6 +181,28 @@ func testSendTransactions(t *testing.T, protocol int) { wg.Wait() } +func TestBlockBodiesDataSanityCheckUncleExtraSize(t *testing.T) { + request := blockBodiesData{{ + Uncles: []*types.Header{{ + Extra: bytes.Repeat([]byte{0x01}, 20*1024+1), + }}, + }} + + if err := request.sanityCheck(); err == nil { + t.Fatal("expected oversized uncle extra data to fail sanity check") + } +} + +func TestBlockHeadersDataSanityCheckExtraSize(t *testing.T) { + headers := []*types.Header{{ + Extra: bytes.Repeat([]byte{0x01}, 20*1024+1), + }} + + if err := types.SanityCheckHeaders(headers); err == nil { + t.Fatal("expected oversized header extra data to fail sanity check") + } +} + // Tests that the custom union field encoder and decoder works correctly. func TestGetBlockHeadersDataEncodeDecode(t *testing.T) { // Create a "random" hash for testing