Moderate severityOSV Advisory· Published Jan 14, 2026· Updated Jan 22, 2026
Undici has an unbounded decompression chain in HTTP responses on Node.js Fetch API via Content-Encoding leads to resource exhaustion
CVE-2026-22036
Description
Undici is an HTTP/1.1 client for Node.js. Prior to 7.18.0 and 6.23.0, the number of links in the decompression chain is unbounded and the default maxHeaderSize allows a malicious server to insert thousands compression steps leading to high CPU usage and excessive memory allocation. This vulnerability is fixed in 7.18.0 and 6.23.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
undicinpm | >= 7.0.0, < 7.18.2 | 7.18.2 |
undicinpm | < 6.23.0 | 6.23.0 |
Affected products
1Patches
1b04e3cbb569cfix(decompress): limit Content-Encoding chain to 5 to prevent resource exhaustion (#4729)
2 files changed · +129 −0
lib/interceptor/decompress.js+8 −0 modified@@ -64,10 +64,18 @@ class DecompressHandler extends DecoratorHandler { * * @param {string} encodings - Comma-separated list of content encodings * @returns {Array<DecompressorStream>} - Array of decompressor streams + * @throws {Error} - If the number of content-encodings exceeds the maximum allowed */ #createDecompressionChain (encodings) { const parts = encodings.split(',') + // Limit the number of content-encodings to prevent resource exhaustion. + // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206). + const maxContentEncodings = 5 + if (parts.length > maxContentEncodings) { + throw new Error(`too many content-encodings in response: ${parts.length}, maximum allowed is ${maxContentEncodings}`) + } + /** @type {DecompressorStream[]} */ const decompressors = []
test/interceptors/decompress.js+121 −0 modified@@ -943,6 +943,127 @@ test('should behave like fetch() for compressed responses', async t => { await t.completed }) +// CVE fix: Limit the number of content-encodings to prevent resource exhaustion +// Similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206) +const MAX_CONTENT_ENCODINGS = 5 + +test(`should allow exactly ${MAX_CONTENT_ENCODINGS} content-encodings`, async t => { + t = tspl(t, { plan: 3 }) + + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + // Use identity encodings (no actual compression) for simplicity + const encodings = Array(MAX_CONTENT_ENCODINGS).fill('identity').join(', ') + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Content-Encoding': encodings + }) + res.end('test') + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client( + `http://localhost:${server.address().port}` + ).compose(createDecompressInterceptor()) + + after(async () => { + await client.close() + server.close() + await once(server, 'close') + }) + + const response = await client.request({ + method: 'GET', + path: '/' + }) + + // With 5 identity encodings, the interceptor should pass through (identity is not in supportedEncodings) + t.equal(response.statusCode, 200) + t.ok(response.headers['content-encoding'], 'content-encoding header should be preserved for identity') + t.equal(await response.body.text(), 'test') + + await t.completed +}) + +test(`should reject more than ${MAX_CONTENT_ENCODINGS} content-encodings`, async t => { + t = tspl(t, { plan: 1 }) + + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + const encodings = Array(MAX_CONTENT_ENCODINGS + 1).fill('gzip').join(', ') + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Content-Encoding': encodings + }) + res.end('test') + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client( + `http://localhost:${server.address().port}` + ).compose(createDecompressInterceptor()) + + after(async () => { + await client.close() + server.close() + await once(server, 'close') + }) + + try { + const response = await client.request({ + method: 'GET', + path: '/' + }) + await response.body.text() + t.fail('Should have thrown an error') + } catch (err) { + t.ok(err.message.includes('content-encoding'), 'Error should mention content-encoding') + } + + await t.completed +}) + +test('should reject excessive content-encoding chains', async t => { + t = tspl(t, { plan: 1 }) + + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + const encodings = Array(100).fill('gzip').join(', ') + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Content-Encoding': encodings + }) + res.end('test') + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client( + `http://localhost:${server.address().port}` + ).compose(createDecompressInterceptor()) + + after(async () => { + await client.close() + server.close() + await once(server, 'close') + }) + + try { + const response = await client.request({ + method: 'GET', + path: '/' + }) + await response.body.text() + t.fail('Should have thrown an error') + } catch (err) { + t.ok(err.message.includes('content-encoding'), 'Error should mention content-encoding') + } + + await t.completed +}) + test('should work with global dispatcher for both fetch() and request()', async t => { t = tspl(t, { plan: 8 })
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-g9mf-h72j-4rw9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-22036ghsaADVISORY
- github.com/nodejs/undici/commit/b04e3cbb569c1596f86c108e9b52c79d8475dcb3ghsax_refsource_MISCWEB
- github.com/nodejs/undici/security/advisories/GHSA-g9mf-h72j-4rw9ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.