VYPR
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.

PackageAffected versionsPatched versions
undicinpm
>= 7.0.0, < 7.18.27.18.2
undicinpm
< 6.23.06.23.0

Affected products

1

Patches

1
b04e3cbb569c

fix(decompress): limit Content-Encoding chain to 5 to prevent resource exhaustion (#4729)

https://github.com/nodejs/undiciMatteo CollinaJan 6, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.