GHSA-96h4-vgxj-gvm2
Nest: Unbounded memory growth in the NestJS TCP microservice transport
Summary
## Summary A peer that can open a TCP connection to a NestJS microservice using the built-in TCP transport can make the server process allocate memory without limit, on either side of the connection, until the process is killed by the OS or by its container memory limit. No authentication, no credentials, and no valid message are required. Applications are affected only if they start a microservice with `Transport.TCP` and the transport's port is reachable by an untrusted peer. ## Affected versions | Package | Affected | Patched | |---|---|---| | `@nestjs/microservices` | `>= 12.0.0, # `. A peer may declare a length, send part of the payload, and then stop. `JsonSocket` keeps the partial payload buffered while it waits for the rest, and nothing ever reclaimed it: there was no socket timeout, no cap on the number of connections, and no tracking of accepted sockets. `maxBufferSize` did not close this. It caps a single connection, defaulting to 128M characters, and is enforced per connection, so the effective ceiling was the number of connections an attacker chose to open. Ten connections that each send 20MB and then go silent: | | rss | heapUsed | |---|---|---| | baseline | 152.3MB | 28.1MB | | after 200MB sent, all stalled | 561.2MB | 229.5MB | The memory was held for as long as the connections stayed open. Closing them released it, so a peer could hold it indefinitely at negligible cost to itself. ### Response backpressure was ignored `JsonSocket#handleSend` discarded the return value of `socket.write`. A peer that issues requests and never reads the responses therefore made the process queue every response in memory, with no ceiling and no signal to stop producing more. Thirty requests from a client whose stream is paused, against a handler returning an 8MB payload, grew rss from 152.4MB to 419.1MB. The amplification here is roughly 1:1, because the response size is set by the application rather than by the attacker; the defect is that the server buffers all of it rather than applying backpressure or giving up. ## Impact An unauthenticated peer that can reach the transport's port can drive the process to an out-of-memory kill. Under a container memory limit this is fast and repeatable, and it restarts the pod rather than merely degrading it. There is no confidentiality or integrity impact. Nothing is read, written, or executed; the failure mode is availability only. Applications are not affected if the TCP transport is not used, or if the transport's port is reachable only by trusted peers. ## Patches Upgrade `@nestjs/microservices` to **12.0.3** or **11.2.5**. Three changes landed together: - **A stall timer drops a peer that stops sending mid-packet.** It is armed only while a packet is partially received and is refreshed on every read, so a slow but progressing transfer is never interrupted and a peer sitting idle between packets is left alone. Configurable as `incompleteMessageTimeout`, default 30000ms. - **Reading is suspended while a peer's outgoing buffer is backed up** and resumes on `drain`, so a peer that stops reading cannot keep issuing requests. - **Responses queued for a peer that reads nothing are capped.** Suspending reads is not sufficient on its own, because message handlers are asynchronous: a burst of pipelined requests is dispatched before the first response is written, and those responses still queue. Configurable as `maxSendBufferSize`, default 128MB. Both limits are confi
Affected packages
| Ecosystem | Package | Affected versions | Fixed versions |
|---|---|---|---|
| npm | @nestjs/microservices | — | 11.2.5, 12.0.3 |
Remediation: Upgrade to 11.2.5 or later.
References
Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.