OSV 1.4.0 · github-reviewed · 修改于 2026-09-02 23:28
发布时间
2026-09-01 06:36
GitHub 审查时间
2026-09-01 06:36
NVD 发布时间
—
源文件
advisories/github-reviewed/2026/08/GHSA-rgwj-5xj2-c3m3/GHSA-rgwj-5xj2-c3m3.json
File: lib/compressed_protocol.js
Line: 43 (zlib.inflate(body, (err, data) => { ... }) inside handleCompressedPacket)
When a connection is created with compress: true (and the server advertises CLIENT_COMPRESS), every incoming packet is unwrapped by handleCompressedPacket() in lib/compressed_protocol.js, which calls:
zlib.inflate(body, (err, data) => { ... });
No options object (in particular, no maxOutputLength) is passed. Node's zlib convenience methods default maxOutputLength to buffer.kMaxLength, which on this platform is Number.MAX_SAFE_INTEGER — i.e. effectively unbounded until the process runs out of memory. The 3-byte "length of payload before compression" field in the compressed-packet header is read (packet.readInt24()) but is only used to branch on !== 0; it is never used to cap or validate the actual inflate output size, and the real decompressed size is determined purely by the attacker-supplied deflate stream.
Because DEFLATE can reach compression ratios over 1000:1 for crafted repetitive input, an attacker who controls (or MITMs, on a non-TLS connection) the MySQL server endpoint can send a single small compressed packet that expands to gigabytes in the client's memory — a classic decompression-bomb / "zip bomb" applied to MySQL's client-compression protocol.
mysql2/mysql2/promise using compress: true (a documented option for reducing bandwidth, commonly used for cloud/WAN DB connections).zlib.inflate() starts allocating memory for the full decompressed output with no ceiling.Denial of Service of the client application (process crash / OOM) — not the database itself. No authentication bypass or data exposure. Requires compress: true plus a malicious/compromised server or MITM position.
function handleCompressedPacket(packet) {
const connection = this;
const deflatedLength = packet.readInt24();
const body = packet.readBuffer();
if (deflatedLength !== 0) {
connection.inflateQueue.push((task) => {
zlib.inflate(body, (err, data) => {
if (err) {
connection._handleNetworkError(err);
return;
}
connection._bumpCompressedSequenceId(packet.numPackets);
connection._inflatedPacketsParser.execute(data);
task.done();
});
});
} else {
...
}
}
const MAX_INFLATED_PACKET_SIZE = 1 * 1024 * 1024 * 1024; // e.g. 1 GiB, ideally configurable
zlib.inflate(body, { maxOutputLength: MAX_INFLATED_PACKET_SIZE }, (err, data) => {
if (err) {
connection._handleNetworkError(err);
return;
}
...
});
maxOutputLength makes zlib.inflate abort with ERR_BUFFER_TOO_LARGE as soon as the decompressed size would exceed the cap, routing into the exact same (already-existing) err → connection._handleNetworkError(err) path, so no new error-handling logic is required.
Dynamically confirmed on v3.23.0 (HEAD) using a minimal rogue "MySQL server" built on node-mysql2's own server-mode helpers (mysql.createServer, Packets.Handshake, connection.writeOk()). The rogue server completes a real handshake advertising CLIENT_COMPRESS, then writes one raw compressed frame (509,604 bytes on the wire — a zlib deflate of 500 MB of zero bytes, ratio 1028.8:1) directly to the socket. A normal mysql.createConnection({ ..., compress: true }) victim client — which never issues any query — had its RSS grow from 74.3 MB to 1115.0 MB after receiving that single packet, before erroring out with PROTOCOL_UNEXPECTED_PACKET once the client tried to parse the inflated zero-filled buffer as MySQL packets. The memory allocation happens unconditionally before any content validation.