{"id":"GHSA-96h4-vgxj-gvm2","kind":"vulnerability","aliases":["GHSA-96h4-vgxj-gvm2"],"related":[],"title":"Unbounded memory growth in the NestJS TCP microservice transport","description":"## Summary\r\n\r\nA peer that can open a TCP connection to a NestJS microservice using the built-in TCP\r\ntransport can make the server process allocate memory without limit, on either side of the\r\nconnection, until the process is killed by the OS or by its container memory limit. No\r\nauthentication, no credentials, and no valid message are required.\r\n\r\nApplications are affected only if they start a microservice with `Transport.TCP` and the\r\ntransport's port is reachable by an untrusted peer.\r\n\r\n## Severity\r\n\r\nProposed: **Moderate** — `CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` (6.5)\r\n\r\nThe TCP transport is documented for service-to-service communication on an internal\r\nnetwork, which is why the adjacent-network vector is proposed rather than the network one.\r\nOperators who publish the transport's port to an untrusted network directly, without an L4\r\npolicy in front of it, should treat this as `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H`\r\n(7.5, High) for their own deployment.\r\n\r\nCWE-770 (Allocation of Resources Without Limits or Throttling), CWE-400 (Uncontrolled\r\nResource Consumption).\r\n\r\n## Affected versions\r\n\r\n| Package | Affected | Patched |\r\n|---|---|---|\r\n| `@nestjs/microservices` | `>= 12.0.0, < 12.0.3` | **12.0.3** |\r\n| `@nestjs/microservices` | `< 11.2.5` | **11.2.5** |\r\n\r\nThe same code is present in the 10.x line and earlier. Those lines are end-of-life and\r\nwill not receive a patch; upgrade to a supported major.\r\n\r\nOnly `@nestjs/microservices` is affected. No other package needs updating for this issue.\r\n\r\n## Details\r\n\r\nTwo independent paths let one peer grow the heap without bound.\r\n\r\n### Partial packets are buffered with nothing to reap them\r\n\r\nThe TCP transport frames messages as `<length>#<payload>`. A peer may declare a length,\r\nsend part of the payload, and then stop. `JsonSocket` keeps the partial payload buffered\r\nwhile it waits for the rest, and nothing ever reclaimed it: there was no socket timeout, no\r\ncap on the number of connections, and no tracking of accepted sockets.\r\n\r\n`maxBufferSize` did not close this. It caps a single connection, defaulting to 128M\r\ncharacters, and is enforced per connection, so the effective ceiling was the number of\r\nconnections an attacker chose to open.\r\n\r\nTen connections that each send 20MB and then go silent:\r\n\r\n| | rss | heapUsed |\r\n|---|---|---|\r\n| baseline | 152.3MB | 28.1MB |\r\n| after 200MB sent, all stalled | 561.2MB | 229.5MB |\r\n\r\nThe memory was held for as long as the connections stayed open. Closing them released it,\r\nso a peer could hold it indefinitely at negligible cost to itself.\r\n\r\n### Response backpressure was ignored\r\n\r\n`JsonSocket#handleSend` discarded the return value of `socket.write`. A peer that issues\r\nrequests and never reads the responses therefore made the process queue every response in\r\nmemory, with no ceiling and no signal to stop producing more.\r\n\r\nThirty requests from a client whose stream is paused, against a handler returning an 8MB\r\npayload, grew rss from 152.4MB to 419.1MB. The amplification here is roughly 1:1, because\r\nthe response size is set by the application rather than by the attacker; the defect is that\r\nthe server buffers all of it rather than applying backpressure or giving up.\r\n\r\n## Impact\r\n\r\nAn unauthenticated peer that can reach the transport's port can drive the process to an\r\nout-of-memory kill. Under a container memory limit this is fast and repeatable, and it\r\nrestarts the pod rather than merely degrading it.\r\n\r\nThere is no confidentiality or integrity impact. Nothing is read, written, or executed;\r\nthe failure mode is availability only.\r\n\r\nApplications are not affected if the TCP transport is not used, or if the transport's port\r\nis reachable only by trusted peers.\r\n\r\n## Patches\r\n\r\nUpgrade `@nestjs/microservices` to **12.0.3** or **11.2.5**.\r\n\r\nThree changes landed together:\r\n\r\n- **A stall timer drops a peer that stops sending mid-packet.** It is armed only while a\r\n  packet is partially received and is refreshed on every read, so a slow but progressing\r\n  transfer is never interrupted and a peer sitting idle between packets is left alone.\r\n  Configurable as `incompleteMessageTimeout`, default 30000ms.\r\n- **Reading is suspended while a peer's outgoing buffer is backed up** and resumes on\r\n  `drain`, so a peer that stops reading cannot keep issuing requests.\r\n- **Responses queued for a peer that reads nothing are capped.** Suspending reads is not\r\n  sufficient on its own, because message handlers are asynchronous: a burst of pipelined\r\n  requests is dispatched before the first response is written, and those responses still\r\n  queue. Configurable as `maxSendBufferSize`, default 128MB.\r\n\r\nBoth limits are configurable on the server and on the client, and either can be disabled by\r\nsetting it to `0`.\r\n\r\n### Behaviour change\r\n\r\nBoth limits are enabled by default, following the precedent set by `maxBufferSize`. A peer\r\nthat stops sending mid-packet for 30 seconds, or that lets more than 128MB of responses\r\nqueue unread on a single connection, is now disconnected where previously it was not.\r\n\r\nThe send cap can also drop a peer that is genuinely reading, if responses are produced\r\nfaster than the socket drains for long enough. The default is set high for that reason.\r\nApplications that stream very large responses to deliberately slow consumers should raise\r\n`maxSendBufferSize` or set it to `0`.\r\n\r\n## Workarounds\r\n\r\nFor anyone who cannot upgrade immediately:\r\n\r\n- **Restrict who can reach the port.** The TCP transport is designed for trusted\r\n  service-to-service traffic. A network policy, security group, or L4 firewall that limits\r\n  the port to known peers removes the exposure entirely, and is worth doing regardless of\r\n  version.\r\n- **Lower `maxBufferSize`.** This reduces what a single connection can pin on the receive\r\n  side. It does not bound the total, because the limit is per connection and the number of\r\n  connections is not capped, and it does nothing for the response-queueing path.\r\n- **Supply a custom `socketClass`.** An application-provided socket class can implement its\r\n  own idle timeout and backpressure handling. This is the only pre-patch mitigation that\r\n  addresses the sending side.\r\n\r\nSetting a process memory limit does not mitigate this. It converts the failure from an\r\nout-of-memory kill of the host into an out-of-memory kill of the process.\r\n\r\n## Credit\r\n\r\nReported by Salman Aljardan, who provided a detailed write-up and self-contained proofs of\r\nconcept for each issue, and coordinated disclosure.\r\n\r\n## References\r\n\r\n- Fix, 12.x, receive and send side bounds: https://github.com/nestjs/nest/pull/17746\r\n- Fix, 12.x, connection teardown on shutdown: https://github.com/nestjs/nest/pull/17745\r\n- Backport, 11.x: https://github.com/nestjs/nest/pull/17749\r\n- Release 12.0.3: https://github.com/nestjs/nest/releases/tag/v12.0.3","summary":"A peer that can open a TCP connection to a NestJS microservice using the built-in TCP transport can make the process allocate memory without limit until it is killed by the OS or its container limit. No credentials or valid messages are needed.","severity":"medium","cvss":null,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"@nestjs/microservices","note":"< 12.0.3 || < 11.2.5","fixed":["12.0.3","11.2.5"],"affected":"< 12.0.3; < 11.2.5","ecosystem":""}],"ecosystems":[],"tags":["kubernetes","microservices","nestjs","node","package","self-hosted"],"not_affected_if":["l4-policy-in-front","port-trusted-peers-only","tcp-transport-not-used"],"how_to_check":"- Check the installed version of `@nestjs/microservices` in the project's lockfile or with `npm ls @nestjs/microservices`.\n- Look for `Transport.TCP` in microservice bootstrap code and confirm whether its port is reachable from untrusted networks.","fix":"- `@nestjs/microservices` 12.x: upgrade to `12.0.3`\n- `@nestjs/microservices` 11.x: upgrade to `11.2.5`\n- 10.x and earlier are end-of-life and receive no patch; the sources say to upgrade to a supported major.","status":"open","published_at":"2026-09-15T08:38:44+00:00","first_seen_at":"2026-11-09T22:53:18+00:00","triaged_at":"2026-11-09T23:01:24+00:00","updated_at":"2026-11-09T23:01:24+00:00","category":"package","confidence":0.9,"triage":{"at":"2026-11-09T23:01:24Z","model":"claude-haiku-5-5","output":{"fix":"- `@nestjs/microservices` 12.x: upgrade to `12.0.3`\n- `@nestjs/microservices` 11.x: upgrade to `11.2.5`\n- 10.x and earlier are end-of-life and receive no patch; the sources say to upgrade to a supported major.","reach":"network","title":"NestJS microservices: TCP transport lets a peer exhaust server memory","impact":"medium","summary":"A peer that can open a TCP connection to a NestJS microservice using the built-in TCP transport can make the process allocate memory without limit until it is killed by the OS or its container limit. No credentials or valid messages are needed.","audience":"developers","category":"package","nicknames":[],"who_cares":["nestjs","node","microservices","self-hosted","kubernetes"],"confidence":0.9,"fix_status":"available","needs_auth":"no","worst_case":"Unauthenticated peer drives the server process to an out-of-memory kill","analyst_note":"","how_to_check":"- Check the installed version of `@nestjs/microservices` in the project's lockfile or with `npm ls @nestjs/microservices`.\n- Look for `Transport.TCP` in microservice bootstrap code and confirm whether its port is reachable from untrusted networks.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["tcp-transport-not-used","port-trusted-peers-only","l4-policy-in-front"],"who_is_affected":"Applications are exposed only if they start a microservice with the built-in TCP transport and its port is reachable by an untrusted peer. Services on a trusted internal network behind a network policy or firewall are not exposed to outside peers. The sources say the same code exists in 10.x and earlier.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"medium"},"cost_usd":0.001016,"input_tokens":6914,"output_tokens":649,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-400':251C '-770':242C '/av':160C,226C '1':542C,543C '10':274C '11.2.5':265C,266C,665C '12.0.0':261C '12.0.3':262C,263C,663C '128m':397C '152.3':431C '152.4':532C '200mb':436C '20mb':423C '229.5':442C '28.1':433C '3.1':159C,225C '419.1':535C '561.2':440C '6.5':169C '7.5':235C '8mb':527C '96h4':12A,17A 'a/ac':161C 'accept':383C 'adjac':190C 'adjacent-network':189C 'affect':135C,255C,258C,300C,639C 'alloc':52B,99C,243C 'amplif':538C 'appli':570C 'applic':133C,552C,636C 'arm':684C 'attack':414C,557C 'authent':124C 'avail':634C 'backpressur':473C,571C 'baselin':430C 'bound':320C 'buffer':324C,355C,564C 'built':44B,90C 'built-in':43B,89C 'cap':373C,391C 'ceil':407C,505C 'chang':667C 'charact':398C 'chose':415C 'client':517C 'close':388C,456C 'code':269C 'communic':180C 'confidenti':620C 'connect':36B,82C,108C,378C,394C,403C,412C,419C,453C 'consumpt':254C 'contain':65B,120C,599C 'cost':469C 'could':463C 'credenti':68B,126C 'cvss':158C,224C 'cwe':241C,250C 'declar':339C 'default':395C 'defect':559C 'degrad':615C 'deploy':240C 'detail':309C 'direct':211C 'discard':478C 'document':174C 'drive':587C 'drop':673C 'earlier':278C 'effect':406C 'either':104C 'end':283C 'end-of-lif':282C 'enforc':401C 'ever':364C 'everi':499C,696C 'execut':629C 'failur':631C 'fast':604C 'frame':333C 'front':217C 'ghsa':11A,16A 'ghsa-96h4-vgxj-gvm2':10A,15A 'give':573C 'go':426C 'grew':529C 'grow':316C 'growth':3A 'gvm2':14A,19A 'h':168C,234C 'handler':524C 'handlesend':477C 'heap':318C 'heapus':429C 'held':447C 'high':236C 'hold':464C 'host':28B 'ignor':475C 'impact':575C,623C 'indefinit':466C 'independ':311C 'integr':622C 'intern':183C 'issu':308C,487C 'jsonsocket':350C,476C 'keep':351C 'kill':59B,113C,596C 'kubernet':21B 'l/pr':162C,228C 'l4':214C 'land':668C 'length':341C 'let':313C 'life':285C 'limit':55B,66B,102C,122C,247C,601C 'line':276C,280C 'long':450C 'made':495C 'major':296C 'make':49B,95C 'maxbuffers':385C 'may':338C 'mb':432C,434C,441C,443C,533C,536C 'memori':2A,53B,100C,121C,445C,502C,595C,600C 'mere':614C 'messag':71B,130C,334C 'microservic':8A,22B,40B,86C,141C 'mid':680C 'mid-packet':679C 'mode':632C 'moder':157C 'n/a':167C,233C 'n/ac':227C 'n/i':166C,232C 'n/s':164C,230C 'n/ui':163C,229C 'need':73B,304C 'neglig':468C 'nestj':6A,23B,39B,85C 'nestjs/microservices':20A,260C,264C,298C,661C 'network':184C,191C,198C,210C 'never':490C 'node':24B 'noth':326C,363C,624C 'number':376C,410C 'one':199C,314C 'open':33B,79C,417C,455C 'oper':200C 'os':62B,116C 'out-of-memori':592C 'packag':25B,257C,303C 'packet':322C,681C,688C 'part':343C 'partial':321C,353C,690C 'patch':259C,291C,659C 'path':312C 'paus':521C 'payload':346C,354C,528C 'peer':30B,76C,154C,315C,337C,462C,485C,578C,658C,675C 'per':402C 'pod':611C 'polici':215C 'port':148C,206C,585C,652C 'present':271C 'process':51B,98C,111C,497C,589C 'produc':511C 'propos':156C,194C 'publish':202C 'queue':498C 'r':697C 'rather':195C,553C,568C,612C 'reach':581C 'reachabl':150C,654C 'read':491C,626C 'reap':328C 'receiv':289C,691C 'reclaim':365C 'refresh':694C 'releas':458C 'repeat':606C 'request':488C,514C 'requir':132C 'resourc':245C,253C 'respons':472C,493C,500C,546C 'rest':361C 'restart':609C 'return':480C,525C 'rough':541C 'rss':428C,530C 'self':27B 'self-host':26B 'send':342C,422C,678C 'sent':437C 'server':97C,563C 'servic':177C,179C 'service-to-servic':176C 'set':549C 'sever':155C 'side':105C 'signal':508C 'silent':427C 'singl':393C 'size':547C 'socket':370C,384C 'socket.write':483C 'stall':439C,671C 'start':139C 'stay':454C 'stop':349C,510C,677C 'stream':519C 'summari':74C 'support':295C 'tcp':7A,35B,46B,81C,92C,171C,331C,642C 'ten':418C 'therefor':494C 'thirti':513C 'three':666C 'throttl':249C 'timeout':371C 'timer':672C 'togeth':669C 'track':381C 'transport':9A,47B,93C,146C,172C,204C,332C,583C,643C,650C 'transport.tcp':143C 'treat':221C 'trust':657C 'two':310C 'u/c':165C,231C 'unauthent':577C 'unbound':1A 'uncontrol':252C 'untrust':153C,209C 'updat':305C 'upgrad':292C,660C 'use':41B,87C,646C 'valid':70B,129C 'valu':481C 'vector':192C 'version':256C 'vgxj':13A,18A 'wait':358C 'whose':518C 'without':54B,101C,212C,246C,319C 'written':627C 'x':275C","nicknames":[],"mitigations":[],"who_affected":"Applications are exposed only if they start a microservice with the built-in TCP transport and its port is reachable by an untrusted peer. Services on a trusted internal network behind a network policy or firewall are not exposed to outside peers. The sources say the same code exists in 10.x and earlier.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["microservices"],"ptokens":["microservices","nestjs"],"labels":["kubernetes","medium","microservices","nestjs","node","package","selfhosted"],"campaign_type":null,"search_a":"'96h4':12A,17A 'ghsa':11A,16A 'ghsa-96h4-vgxj-gvm2':10A,15A 'growth':3A 'gvm2':14A,19A 'memori':2A 'microservic':8A 'nestj':6A 'nestjs/microservices':20A 'tcp':7A 'transport':9A 'unbound':1A 'vgxj':13A,18A","search_b":"'alloc':52B 'built':44B 'built-in':43B 'connect':36B 'contain':65B 'credenti':68B 'host':28B 'kill':59B 'kubernet':21B 'limit':55B,66B 'make':49B 'memori':53B 'messag':71B 'microservic':22B,40B 'need':73B 'nestj':23B,39B 'node':24B 'open':33B 'os':62B 'packag':25B 'peer':30B 'process':51B 'self':27B 'self-host':26B 'tcp':35B,46B 'transport':47B 'use':41B 'valid':70B 'without':54B","search_c":"'-400':251C '-770':242C '/av':160C,226C '1':542C,543C '10':274C '11.2.5':265C,266C,665C '12.0.0':261C '12.0.3':262C,263C,663C '128m':397C '152.3':431C '152.4':532C '200mb':436C '20mb':423C '229.5':442C '28.1':433C '3.1':159C,225C '419.1':535C '561.2':440C '6.5':169C '7.5':235C '8mb':527C 'a/ac':161C 'accept':383C 'adjac':190C 'adjacent-network':189C 'affect':135C,255C,258C,300C,639C 'alloc':99C,243C 'amplif':538C 'appli':570C 'applic':133C,552C,636C 'arm':684C 'attack':414C,557C 'authent':124C 'avail':634C 'backpressur':473C,571C 'baselin':430C 'bound':320C 'buffer':324C,355C,564C 'built':90C 'built-in':89C 'cap':373C,391C 'ceil':407C,505C 'chang':667C 'charact':398C 'chose':415C 'client':517C 'close':388C,456C 'code':269C 'communic':180C 'confidenti':620C 'connect':82C,108C,378C,394C,403C,412C,419C,453C 'consumpt':254C 'contain':120C,599C 'cost':469C 'could':463C 'credenti':126C 'cvss':158C,224C 'cwe':241C,250C 'declar':339C 'default':395C 'defect':559C 'degrad':615C 'deploy':240C 'detail':309C 'direct':211C 'discard':478C 'document':174C 'drive':587C 'drop':673C 'earlier':278C 'effect':406C 'either':104C 'end':283C 'end-of-lif':282C 'enforc':401C 'ever':364C 'everi':499C,696C 'execut':629C 'failur':631C 'fast':604C 'frame':333C 'front':217C 'give':573C 'go':426C 'grew':529C 'grow':316C 'h':168C,234C 'handler':524C 'handlesend':477C 'heap':318C 'heapus':429C 'held':447C 'high':236C 'hold':464C 'ignor':475C 'impact':575C,623C 'indefinit':466C 'independ':311C 'integr':622C 'intern':183C 'issu':308C,487C 'jsonsocket':350C,476C 'keep':351C 'kill':113C,596C 'l/pr':162C,228C 'l4':214C 'land':668C 'length':341C 'let':313C 'life':285C 'limit':102C,122C,247C,601C 'line':276C,280C 'long':450C 'made':495C 'major':296C 'make':95C 'maxbuffers':385C 'may':338C 'mb':432C,434C,441C,443C,533C,536C 'memori':100C,121C,445C,502C,595C,600C 'mere':614C 'messag':130C,334C 'microservic':86C,141C 'mid':680C 'mid-packet':679C 'mode':632C 'moder':157C 'n/a':167C,233C 'n/ac':227C 'n/i':166C,232C 'n/s':164C,230C 'n/ui':163C,229C 'need':304C 'neglig':468C 'nestj':85C 'nestjs/microservices':260C,264C,298C,661C 'network':184C,191C,198C,210C 'never':490C 'noth':326C,363C,624C 'number':376C,410C 'one':199C,314C 'open':79C,417C,455C 'oper':200C 'os':116C 'out-of-memori':592C 'packag':257C,303C 'packet':322C,681C,688C 'part':343C 'partial':321C,353C,690C 'patch':259C,291C,659C 'path':312C 'paus':521C 'payload':346C,354C,528C 'peer':76C,154C,315C,337C,462C,485C,578C,658C,675C 'per':402C 'pod':611C 'polici':215C 'port':148C,206C,585C,652C 'present':271C 'process':98C,111C,497C,589C 'produc':511C 'propos':156C,194C 'publish':202C 'queue':498C 'r':697C 'rather':195C,553C,568C,612C 'reach':581C 'reachabl':150C,654C 'read':491C,626C 'reap':328C 'receiv':289C,691C 'reclaim':365C 'refresh':694C 'releas':458C 'repeat':606C 'request':488C,514C 'requir':132C 'resourc':245C,253C 'respons':472C,493C,500C,546C 'rest':361C 'restart':609C 'return':480C,525C 'rough':541C 'rss':428C,530C 'send':342C,422C,678C 'sent':437C 'server':97C,563C 'servic':177C,179C 'service-to-servic':176C 'set':549C 'sever':155C 'side':105C 'signal':508C 'silent':427C 'singl':393C 'size':547C 'socket':370C,384C 'socket.write':483C 'stall':439C,671C 'start':139C 'stay':454C 'stop':349C,510C,677C 'stream':519C 'summari':74C 'support':295C 'tcp':81C,92C,171C,331C,642C 'ten':418C 'therefor':494C 'thirti':513C 'three':666C 'throttl':249C 'timeout':371C 'timer':672C 'togeth':669C 'track':381C 'transport':93C,146C,172C,204C,332C,583C,643C,650C 'transport.tcp':143C 'treat':221C 'trust':657C 'two':310C 'u/c':165C,231C 'unauthent':577C 'uncontrol':252C 'untrust':153C,209C 'updat':305C 'upgrad':292C,660C 'use':87C,646C 'valid':129C 'valu':481C 'vector':192C 'version':256C 'wait':358C 'whose':518C 'without':101C,212C,246C,319C 'written':627C 'x':275C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-09-15T08:38:44+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-96h4-vgxj-gvm2","url":"https://github.com/nestjs/nest/security/advisories/GHSA-96h4-vgxj-gvm2","severity":"medium","published_at":"2026-09-15T08:38:44+00:00","modified_at":"2026-09-15T08:38:44+00:00","first_seen_at":"2026-11-09T22:53:18+00:00","fetched_at":"2026-11-09T22:53:18+00:00","note":null}],"documents":[{"id":110470,"url":"https://github.com/nestjs/nest/security/advisories/GHSA-96h4-vgxj-gvm2","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}