GHSA-4f8r-922h-2vgv js-libp2p: Memory DoS via subscription flood of unique topics
SummaryThree cooperating omissions in `@libp2p/gossipsub` allow an unauthenticated single peer to exhaust the Node.js heap of any gossipsub node with default options.**`defaultDecodeRpcLimits.maxSubscriptions = Infinity`** (`packages/gossipsub/src/message/decodeRpc.ts:11`): no decode-level cap on subscription entries per RPC.**`handleReceivedSubscription` is unbounded** (`gossipsub.ts:1009-1021`): every unique topic string creates a new `Map` entry + `Set` object in `this.topics` with no per-peer count limit.**`removePeer` leaves empty Sets** (`gossipsub.ts:782-784`): after peer disconnect, empty Sets are never deleted from `this.topics` thus memory is non-reclaimable within the process lifetime.A single 4MB LP frame carries 349,525 unique topic SUBSCRIBE entries. Each frame causes ~89MB of heap growth (~22x amplification). A Node.js process with a 1.5GB heap limit crashes after ~17 such frames (~68MB total attacker bandwidth, achievable in ~5 seconds at 100Mbps).DetailsDefect 1: `defaultDecodeRpcLimits.maxSubscriptions = Infinity` (`message/decodeRpc.ts:11`)Passed directly to the protobuf decoder at `gossipsub.ts:863`. A single RPC may decode 349,525 SUBSCRIBE entries within the 4MB LP frame with no error.Defect 2: `handleReceivedSubscription` unbounded growth (`gossipsub.ts:1009-1021`)`this.topics` (`Map<TopicStr, Set<PeerIdStr>>`, `gossipsub.ts:141`) has no size limit. No per-peer topic count is tracked. No heartbeat evicts unused entries. A comment at `gossipsub.ts:960` acknowledges the map is "not bounded by topic count", but only for the `allowedTopics != null` branch, the default is `null`.Defect 3: `removePeer` memory leak (`gossipsub.ts:782-784`)After disconnect, `this.topics` retains N empty Sets, one per unique attacker topic. `stop()` (lines 575–602) clears 12 data structures but not `this.topics`. Memory is leaked for the process lifetime.Secondary: the O(topics.size) synchronous scan in `removePeer` grows as `this.topics` accumulates from repeated attacks. After 17 rounds, the scan iterates ~6M entries each time any peer disconnects.Attack pathAttacker dials victim and opens a gossipsub stream.Score 0 > `gossipThreshold = −10` thus subscriptions are processed immediately. No score check gates subscription handling.Attacker constructs an RPC: 349,525 SUBSCRIBE entries with sequential 6-char topics. Total encoded size: 4.00 MB.Victim's `handleReceivedRpc` calls `rpc.subscriptions.forEach(...)` → 349,525 calls to `handleReceivedSubscription` -> `this.topics` grows by 349,525 entries -> ~89MB heap consumed -> ~224ms event-loop blocked.Attacker reconnects. No score decay or penalty applies to subscription RPCs. Repeat.After ~17 rounds (68MB attacker bandwidth): Node.js OOM (Out-Of-Memory) crash.PoC**Steps to reproduce** (confirmed unpatched at HEAD `9eb27be79`):File PoC:Impact**Availability (memory)**: single peer, ~68MB bandwidth -> OOM crash in ~5s at 100Mbps. Non-recoverable within process lifetime thus memory never freed even if attacker disconnects.**Availability (CPU)**: 224ms event-loop block per 4MB subscription RPC (synchronous `forEach`); grows with accumulated attack state.**No score mitigation**: subscription processing has no score check and no score penalty for flooding.**Affected deployments**: any node running `@libp2p/gossipsub` with default options that accepts inbound connections: Lodestar (Ethereum consensus), IPFS pubsub, any `createLibp2p({ services: { pubsub: gossipsub() } })`.**Partial mitigation only**: setting `opts.allowedTopics` caps growth to `allowedTopics.size` topics per attacker; does not fix the memory leak for allowed topics or the O(N) `removePeer` scan.Suggested remediationDelete empty Sets on unsubscribe and disconnect:Clear `this.topics` in `stop()`:
CVSS v3.1 score: 7.5