CVE-2023-25568: Allocation of Resources Without Limits or Throttling (CWE-770)
value: Boxo, formerly known as go-libipfs, is a library for building IPFS applications and implementations. In versions 0.4.0 and 0.5.0, if an attacker is able allocate arbitrary many bytes in the Bitswap server, those allocations are lasting even if the connection is closed. This affects users accepting untrusted connections with the Bitswap server and also affects users using the old API stubs at `github.com/ipfs/go-libipfs/bitswap` because users then transitively import `github.com/ipfs/go-libipfs/bitswap/server`. Boxo versions 0.6.0 and 0.4.1 contain a patch for this issue. As a workaround, those who are using the stub object at `github.com/ipfs/go-libipfs/bitswap` not taking advantage of the features provided by the server can refactor their code to use the new split API that will allow them to run in a client only mode: `github.com/ipfs/go-libipfs/bitswap/client`.
CWE description: The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
CVSS v3 base score: 7.5
Published: 2023-05-10T14:15Z
Repository: https://github.com/ipfs/boxo
Language: Go
Files changed: bitswap/internal/defaults/defaults.go, bitswap/network/ipfs_impl.go, bitswap/options.go, bitswap/server/internal/decision/engine.go, bitswap/server/server.go
Commit date: 2023-02-17 18:06:35 +0100
Commit message: bitswap/server/internal/decision: add filtering on CIDs
- Ignore cids that are too big.
- Kill connection for peers that are using inline CIDs.

Diff: diff --git a/bitswap/internal/defaults/defaults.go b/bitswap/internal/defaults/defaults.go
index 1b0de249..f9494a0d 100644
--- a/bitswap/internal/defaults/defaults.go
+++ b/bitswap/internal/defaults/defaults.go
@@ -1,8 +1,9 @@
 package defaults
 
 import (
+	"encoding/binary"
 	"time"
 )
 
 const (
 	// these requests take at _least_ two minutes at the moment.
@@ -25,6 +26,11 @@ const (
 	// TODO: Does this need to be this large givent that?
 	HasBlockBufferSize = 256
 
 	// Maximum size of the wantlist we are willing to keep in memory.
 	MaxQueuedWantlistEntiresPerPeer = 1024
+
+	// Copied from github.com/ipfs/go-verifcid#maximumHashLength
+	// FIXME: expose this in go-verifcid.
+	MaximumHashLength = 128
+	MaximumAllowedCid = binary.MaxVarintLen64*4 + MaximumHashLength
 )
diff --git a/bitswap/network/ipfs_impl.go b/bitswap/network/ipfs_impl.go
index 356109eb..c796a473 100644
--- a/bitswap/network/ipfs_impl.go
+++ b/bitswap/network/ipfs_impl.go
@@ -368,11 +368,11 @@ func (bsnet *impl) Stop() {
 func (bsnet *impl) ConnectTo(ctx context.Context, p peer.ID) error {
 	return bsnet.host.Connect(ctx, peer.AddrInfo{ID: p})
 }
 
 func (bsnet *impl) DisconnectFrom(ctx context.Context, p peer.ID) error {
-	panic("Not implemented: DisconnectFrom() is only used by tests")
+	return bsnet.host.Network().ClosePeer(p)
 }
 
 // FindProvidersAsync returns a channel of providers for the given key.
 func (bsnet *impl) FindProvidersAsync(ctx context.Context, k cid.Cid, max int) <-chan peer.ID {
 	out := make(chan peer.ID, max)
diff --git a/bitswap/options.go b/bitswap/options.go
index 1e2e0901..9ccf3c4d 100644
--- a/bitswap/options.go
+++ b/bitswap/options.go
@@ -31,10 +31,16 @@ func MaxOutstandingBytesPerPeer(count int) Option {
 
 func MaxQueuedWantlistEntriesPerPeer(count uint) Option {
 	return Option{server.MaxQueuedWantlistEntriesPerPeer(count)}
 }
 
+// MaxCidSize only affects the server.
+// If it is 0 no limit is applied.
+func MaxCidSize(n uint) Option {
+	return Option{server.MaxCidSize(n)}
+}
+
 func TaskWorkerCount(count int) Option {
 	return Option{server.TaskWorkerCount(count)}
 }
 
 func ProvideEnabled(enabled bool) Option {
diff --git a/bitswap/server/internal/decision/engine.go b/bitswap/server/internal/decision/engine.go
index 29ac1aa2..208fef74 100644
--- a/bitswap/server/internal/decision/engine.go
+++ b/bitswap/server/internal/decision/engine.go
@@ -23,10 +23,11 @@ import (
 	"github.com/ipfs/go-peertaskqueue"
 	"github.com/ipfs/go-peertaskqueue/peertask"
 	"github.com/ipfs/go-peertaskqueue/peertracker"
 	process "github.com/jbenet/goprocess"
 	"github.com/libp2p/go-libp2p/core/peer"
+	mh "github.com/multiformats/go-multihash"
 )
 
 // TODO consider taking responsibility for other types of requests. For
 // example, there could be a |cancelQueue| for all of the cancellation
 // messages that need to go out. There could also be a |wantlistQueue| for
@@ -185,10 +186,11 @@ type Engine struct {
 
 	bstoreWorkerCount          int
 	maxOutstandingBytesPerPeer int
 
 	maxQueuedWantlistEntriesPerPeer uint
+	maxCidSize                      uint
 }
 
 // TaskInfo represents the details of a request from a peer.
 type TaskInfo struct {
 	Peer peer.ID
@@ -270,17 +272,24 @@ func WithMaxOutstandingBytesPerPeer(count int) Option {
 	}
 }
 
 // WithMaxQueuedWantlistEntriesPerPeer limits how much individual entries each peer is allowed to send.
 // If a peer send us more than this we will truncate newest entries.
-// It defaults to DefaultMaxQueuedWantlistEntiresPerPeer.
 func WithMaxQueuedWantlistEntriesPerPeer(count uint) Option {
 	return func(e *Engine) {
 		e.maxQueuedWantlistEntriesPerPeer = count
 	}
 }
 
+// WithMaxQueuedWantlistEntriesPerPeer limits how much individual entries each peer is allowed to send.
+// If a peer send us more than this we will truncate newest entries.
+func WithMaxCidSize(n uint) Option {
+	return func(e *Engine) {
+		e.maxCidSize = n
+	}
+}
+
 func WithSetSendDontHave(send bool) Option {
 	return func(e *Engine) {
 		e.sendDontHaves = send
 	}
 }
@@ -355,10 +364,11 @@ func newEngine(
 		activeGauge:                     bmetrics.ActiveEngineGauge(ctx),
 		targetMessageSize:               defaultTargetMessageSize,
 		tagQueued:                       fmt.Sprintf(tagFormat, "queued", uuid.New().String()),
 		tagUseful:                       fmt.Sprintf(tagFormat, "useful", uuid.New().String()),
 		maxQueuedWantlistEntriesPerPeer: defaults.MaxQueuedWantlistEntiresPerPeer,
+		maxCidSize:                      defaults.MaximumAllowedCid,
 	}
 
 	for _, opt := range opts {
 		opt(e)
 	}
@@ -615,11 +625,11 @@ func (e *Engine) Peers() []peer.ID {
 }
 
 // MessageReceived is called when a message is received from a remote peer.
 // For each item in the wantlist, add a want-have or want-block entry to the
 // request queue (this is later popped off by the workerTasks)
-func (e *Engine) MessageReceived(ctx context.Context, p peer.ID, m bsmsg.BitSwapMessage) {
+func (e *Engine) MessageReceived(ctx context.Context, p peer.ID, m bsmsg.BitSwapMessage) (mustKillConnection bool) {
 	entries := m.Wantlist()
 
 	if len(entries) > 0 {
 		log.Debugw("Bitswap engine <- msg", "local", e.self, "from", p, "entryCount", len(entries))
 		for _, et := range entries {
@@ -674,14 +684,44 @@ func (e *Engine) MessageReceived(ctx context.Context, p peer.ID, m bsmsg.BitSwap
 			available = 0
 		}
 		wants = wants[:available]
 	}
 
+	filteredWants := wants[:0] // shift inplace
+
 	for _, entry := range wants {
+		if entry.Cid.Prefix().MhType == mh.IDENTITY {
+			// This is a truely broken client, let's kill the connection.
+			e.lock.Unlock()
+			log.Warnw("peer wants an identity CID", "local", e.self, "remote", p)
+			return true
+		}
+		if e.maxCidSize != 0 && uint(entry.Cid.ByteLen()) > e.maxCidSize {
+			// Ignore requests about CIDs that big.
+			continue
+		}
+
 		e.peerLedger.Wants(p, entry.Entry)
+		filteredWants = append(filteredWants, entry)
+	}
+	clear := wants[len(filteredWants):]
+	for i := range clear {
+		clear[i] = bsmsg.Entry{} // early GC
 	}
+	wants = filteredWants
 	for _, entry := range cancels {
+		if entry.Cid.Prefix().MhType == mh.IDENTITY {
+			// This is a truely broken client, let's kill the connection.
+			e.lock.Unlock()
+			log.Warnw("peer canceled an identity CID", "local", e.self, "remote", p)
+			return true
+		}
+		if e.maxCidSize != 0 && uint(entry.Cid.ByteLen()) > e.maxCidSize {
+			// Ignore requests about CIDs that big.
+			continue
+		}
+
 		log.Debugw("Bitswap engine <- cancel", "local", e.self, "from", p, "cid", entry.Cid)
 		if e.peerLedger.CancelWant(p, entry.Cid) {
 			e.peerRequestQueue.Remove(entry.Cid, p)
 		}
 	}
@@ -763,10 +803,11 @@ func (e *Engine) MessageReceived(ctx context.Context, p peer.ID, m bsmsg.BitSwap
 	// Push entries onto the request queue
 	if len(activeEntries) > 0 {
 		e.peerRequestQueue.PushTasksTruncated(e.maxQueuedWantlistEntriesPerPeer, p, activeEntries...)
 		e.updateMetrics()
 	}
+	return false
 }
 
 // Split the want-have / want-block entries from the cancel entries
 func (e *Engine) splitWantsCancels(es []bsmsg.Entry) ([]bsmsg.Entry, []bsmsg.Entry) {
 	wants := make([]bsmsg.Entry, 0, len(es))
diff --git a/bitswap/server/server.go b/bitswap/server/server.go
index 42445603..7918b73d 100644
--- a/bitswap/server/server.go
+++ b/bitswap/server/server.go
@@ -218,10 +218,21 @@ func MaxQueuedWantlistEntriesPerPeer(count uint) Option {
 	return func(bs *Server) {
 		bs.engineOptions = append(bs.engineOptions, o)
 	}
 }
 
+// MaxCidSize limits how big CIDs we are willing to serve.
+// We will ignore CIDs over this limit.
+// It defaults to [defaults.MaxCidSize].
+// If it is 0 no limit is applied.
+func MaxCidSize(n uint) Option {
+	o := decision.WithMaxCidSize(n)
+	return func(bs *Server) {
+		bs.engineOptions = append(bs.engineOptions, o)
+	}
+}
+
 // HasBlockBufferSize configure how big the new blocks buffer should be.
 func HasBlockBufferSize(count int) Option {
 	if count < 0 {
 		panic("cannot have negative buffer size")
 	}
@@ -509,11 +520,14 @@ func (bs *Server) provideWorker(px process.Process) {
 }
 
 func (bs *Server) ReceiveMessage(ctx context.Context, p peer.ID, incoming message.BitSwapMessage) {
 	// This call records changes to wantlists, blocks received,
 	// and number of bytes transfered.
-	bs.engine.MessageReceived(ctx, p, incoming)
+	mustKillConnection := bs.engine.MessageReceived(ctx, p, incoming)
+	if mustKillConnection {
+		bs.network.DisconnectFrom(ctx, p)
+	}
 	// TODO: this is bad, and could be easily abused.
 	// Should only track *useful* messages in ledger
 
 	if bs.tracer != nil {
 		bs.tracer.MessageReceived(p, incoming)

Vulnerable code: 	panic("Not implemented: DisconnectFrom() is only used by tests")
// It defaults to DefaultMaxQueuedWantlistEntiresPerPeer.
func (e *Engine) MessageReceived(ctx context.Context, p peer.ID, m bsmsg.BitSwapMessage) {
	bs.engine.MessageReceived(ctx, p, incoming)
Fixed code: 	"encoding/binary"

	// Copied from github.com/ipfs/go-verifcid#maximumHashLength
	// FIXME: expose this in go-verifcid.
	MaximumHashLength = 128
	MaximumAllowedCid = binary.MaxVarintLen64*4 + MaximumHashLength
	return bsnet.host.Network().ClosePeer(p)
// MaxCidSize only affects the server.
// If it is 0 no limit is applied.
func MaxCidSize(n uint) Option {
	return Option{server.MaxCidSize(n)}
}

	mh "github.com/multiformats/go-multihash"
	maxCidSize                      uint
// WithMaxQueuedWantlistEntriesPerPeer limits how much individual entries each peer is allowed to send.
// If a peer send us more than this we will truncate newest entries.
func WithMaxCidSize(n uint) Option {
	return func(e *Engine) {
		e.maxCidSize = n
	}
}

		maxCidSize:                      defaults.MaximumAllowedCid,
func (e *Engine) MessageReceived(ctx context.Context, p peer.ID, m bsmsg.BitSwapMessage) (mustKillConnection bool) {
	filteredWants := wants[:0] // shift inplace

		if entry.Cid.Prefix().MhType == mh.IDENTITY {
			// This is a truely broken client, let's kill the connection.
			e.lock.Unlock()
			log.Warnw("peer wants an identity CID", "local", e.self, "remote", p)
			return true
		}
		if e.maxCidSize != 0 && uint(entry.Cid.ByteLen()) > e.maxCidSize {
			// Ignore requests about CIDs that big.
			continue
		}

		filteredWants = append(filteredWants, entry)
	}
	clear := wants[len(filteredWants):]
	for i := range clear {
		clear[i] = bsmsg.Entry{} // early GC
	wants = filteredWants
		if entry.Cid.Prefix().MhType == mh.IDENTITY {
			// This is a truely broken client, let's kill the connection.
			e.lock.Unlock()
			log.Warnw("peer canceled an identity CID", "local", e.self, "remote", p)
			return true
		}
		if e.maxCidSize != 0 && uint(entry.Cid.ByteLen()) > e.maxCidSize {
			// Ignore requests about CIDs that big.
			continue
		}

	return false
// MaxCidSize limits how big CIDs we are willing to serve.
// We will ignore CIDs over this limit.
// It defaults to [defaults.MaxCidSize].
// If it is 0 no limit is applied.
func MaxCidSize(n uint) Option {
	o := decision.WithMaxCidSize(n)
	return func(bs *Server) {
		bs.engineOptions = append(bs.engineOptions, o)
	}
}

	mustKillConnection := bs.engine.MessageReceived(ctx, p, incoming)
	if mustKillConnection {
		bs.network.DisconnectFrom(ctx, p)
	}