nodejs/CVE-2024-22025.patch

155 lines
31 KiB
Diff
Raw Normal View History

From: Matteo Collina <hello@matteocollina.com>
Date: Tue, 6 Feb 2024 16:47:20 +0100
Subject: CVE-2024-22025 zlib: pause stream if outgoing buffer is full
A vulnerability in Node.js has been identified, allowing for a Denial
of Service (DoS) attack through resource exhaustion when using the
fetch() function to retrieve content from an untrusted URL. The
vulnerability stems from the fact that the fetch() function in Node.js
always decodes Brotli, making it possible for an attacker to cause
resource exhaustion when fetching content from an untrusted URL. An
attacker controlling the URL passed into fetch() can exploit this
vulnerability to exhaust memory, potentially leading to process
termination, depending on the system configuration
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs-private/node-private#540
Reviewed-By: Robert Nagy <ronagy@icloud.com>
bug: https://nodejs.org/en/blog/release/v18.19.1
bug-hakerone: https://hackerone.com/reports/2284065
origin: backport, https://github.com/nodejs/node/commit/9052ef43dc2d1b0db340591a9bc9e45a25c01d90.patch
CVE-ID: CVE-2024-22025
---
lib/zlib.js | 33 +++++++++++++++++++++++++--------
test/parallel/test-zlib-brotli-16GB.js | 22 ++++++++++++++++++++++
test/parallel/test-zlib-params.js | 26 ++++++++++++++++----------
3 files changed, 63 insertions(+), 18 deletions(-)
create mode 100644 test/parallel/test-zlib-brotli-16GB.js
diff --git a/lib/zlib.js b/lib/zlib.js
index a3641d6..1900530 100644
--- a/lib/zlib.js
+++ b/lib/zlib.js
@@ -532,10 +532,11 @@ function processCallback() {
self.bytesWritten += inDelta;
const have = handle.availOutBefore - availOutAfter;
+ let streamBufferIsFull = false;
if (have > 0) {
const out = self._outBuffer.slice(self._outOffset, self._outOffset + have);
self._outOffset += have;
- self.push(out);
+ streamBufferIsFull = !self.push(out);
} else {
assert(have === 0, 'have should not go down');
}
@@ -560,13 +561,29 @@ function processCallback() {
handle.inOff += inDelta;
handle.availInBefore = availInAfter;
- this.write(handle.flushFlag,
- this.buffer, // in
- handle.inOff, // in_off
- handle.availInBefore, // in_len
- self._outBuffer, // out
- self._outOffset, // out_off
- self._chunkSize); // out_len
+
+ if (!streamBufferIsFull) {
+ this.write(handle.flushFlag,
+ this.buffer, // in
+ handle.inOff, // in_off
+ handle.availInBefore, // in_len
+ self._outBuffer, // out
+ self._outOffset, // out_off
+ self._chunkSize); // out_len
+ } else {
+ const oldRead = self._read;
+ self._read = (n) => {
+ self._read = oldRead;
+ this.write(handle.flushFlag,
+ this.buffer, // in
+ handle.inOff, // in_off
+ handle.availInBefore, // in_len
+ self._outBuffer, // out
+ self._outOffset, // out_off
+ self._chunkSize); // out_len
+ self._read(n);
+ };
+ }
return;
}
diff --git a/test/parallel/test-zlib-brotli-16GB.js b/test/parallel/test-zlib-brotli-16GB.js
new file mode 100644
index 0000000..68c29a4
--- /dev/null
+++ b/test/parallel/test-zlib-brotli-16GB.js
@@ -0,0 +1,22 @@
+'use strict';
+
+const common = require('../common');
+const { createBrotliDecompress } = require('zlib');
+const strictEqual = require('assert').strictEqual;
+
+// This tiny HEX string is a 16GB file.
+// This test verifies that the stream actually stops.
+/* eslint-disable max-len */
+const content = 'cfffff7ff82700e2b14020f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c32200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc33f0110870500baf7fffcffff877f02200e0b0074effff9ffff0fff04401c1600e8defff3ffff1ffe0980382c00d0bdffe7ffff3ffc1300715800a07bffcfffff7ff82700e2b00040f7fe9ffffffff04f00c4610180eefd3fffffffe19f0088c30200ddfb7ffeffffc
+
+const buf = Buffer.from(content, 'hex');
+
+const decoder = createBrotliDecompress();
+decoder.end(buf);
+
+// We need to wait to verify that the libuv thread pool had time
+// to process the data and the buffer is not empty.
+setTimeout(common.mustCall(() => {
+ // There is only one chunk in the buffer
+ strictEqual(decoder._readableState.buffer.length, 1);
+}), common.platformTimeout(500));
diff --git a/test/parallel/test-zlib-params.js b/test/parallel/test-zlib-params.js
index 293ceec..d6589e4 100644
--- a/test/parallel/test-zlib-params.js
+++ b/test/parallel/test-zlib-params.js
@@ -12,23 +12,29 @@ const deflater = zlib.createDeflate(opts);
const chunk1 = file.slice(0, chunkSize);
const chunk2 = file.slice(chunkSize);
const blkhdr = Buffer.from([0x00, 0x5a, 0x82, 0xa5, 0x7d]);
-const expected = Buffer.concat([blkhdr, chunk2]);
-let actual;
+const blkftr = Buffer.from('010000ffff7dac3072', 'hex');
+const expected = Buffer.concat([blkhdr, chunk2, blkftr]);
+const bufs = [];
+
+function read() {
+ let buf;
+ while ((buf = deflater.read()) !== null) {
+ bufs.push(buf);
+ }
+}
deflater.write(chunk1, function() {
deflater.params(0, zlib.constants.Z_DEFAULT_STRATEGY, function() {
while (deflater.read());
- deflater.end(chunk2, function() {
- const bufs = [];
- let buf;
- while (buf = deflater.read())
- bufs.push(buf);
- actual = Buffer.concat(bufs);
- });
- });
+
+ deflater.on('readable', read);
+
+ deflater.end(chunk2);
+ });
while (deflater.read());
});
process.once('exit', function() {
+ const actual = Buffer.concat(bufs);
assert.deepStrictEqual(actual, expected);
});