GHSA-32mq-hpph-xfvr @libp2p/kad-dht: Unvalidated PUT_VALUE records allow unbounded disk exhaustion on DHT server nodes
SummaryAn unauthenticated remote peer can exhaust the disk storage of any `@libp2p/kad-dht` node running in server mode by sending an unbounded stream of `PUT_VALUE` messages whose keys bypass all content validation. No credentials, no prior relationship, and no protocol deviation beyond a crafted key are required. The victim node's datastore fills until the host disk is exhausted, making the node unavailable.DetailsTwo cooperating defects combine to produce the vulnerability.**Defect 1: `verifyRecord` silent early-return (`packages/kad-dht/src/record/validators.ts:19-21`)**Legitimate DHT keys (`/pk/<multihash>`, `/ipns/<peerId>`) have exactly 3 slash-delimited parts and are routed to registered validators. Any key whose UTF-8 representation splits into fewer than 3 parts, single-byte keys, or any value without two `/` characters, thus, bypasses validation entirely and is written to the datastore unconditionally. There is no audit log and no error returned to the caller.**Defect 2: Unbounded RPC message loop (`packages/kad-dht/src/rpc/index.ts:103-152`)**The inactivity timeout is reset after **every successfully received message**. There is no per-stream message count limit, no per-peer byte budget, and no rate limiter. An attacker who delivers each message within the 10-second window can stream an unlimited number of messages indefinitely.**Combined impact**`DEFAULT_MAX_DATA_LENGTH = 4 MB` per message (from `@libp2p/utils`)`DEFAULT_MAX_INBOUND_STREAMS = 32` concurrent streams per `kad-dht` instanceAttack throughput: 4 MB × unlimited messages × 32 streamsMinimum attacker cost: standard libp2p TLS handshake (no authentication beyond that)**Differential note**: `go-libp2p-kad-dht` enforces `record.Validator.Validate()` per-key at the RPC layer; records with unrecognised namespaces are rejected with an error, not silently stored. This divergence is JS-specific.PoCThe proof-of-concept is a mocha test checked in alongside the package test suite. It uses an in-memory stream pair, thus, no network traffic, no external connections.**File**: `packages/kad-dht/test/rpc/poc-put-value-unvalidated.spec.ts`:**Steps to reproduce** (tested on commit `15eeedba13846e55e8fc3f9e4c49af18fa185ea4`):**Expected output**:**Test 1** (`BYPASS`) confirms that a single `PUT_VALUE` message with a 3-byte raw key stores a 64 KB payload in the victim's datastore with no validation.**Test 2** (`RATE`) confirms that N sequential writes with distinct keys each produce a new datastore entry, demonstrating the absence of any write budget or deduplication defence.Impact**Affected deployments**: any `@libp2p/kad-dht` node in **server mode** (`clientMode: false`). Server mode is the default for nodes with publicly routable addresses; the `kad-dht` module auto-switches to server mode (`kad-dht.ts:340-358`). This includes:IPFS nodes (kubo, Helia, any JS IPFS implementation)libp2p bootstrap nodesAny application exposing a public DHT endpoint**Not affected**: DHT client-mode nodes, `setMode('client')` calls `registrar.unhandle(this.protocol)` which removes the inbound stream handler entirely.**Availability (disk)**: attacker fills the victim's datastore partition. A full datastore prevents the victim from writing new DHT records, peer store entries, or any other application data sharing the same datastore backend (common in IPFS nodes using a shared `repo` datastore). Node becomes unavailable.**No authentication barrier**: the only prerequisite is a successful libp2p connection handshake (TLS). Any publicly reachable node is exposed.**Suggested minimum fix**:
Change the silent early-return to a hard rejection:
CVSS v3.1 score: 7.5