155 lines
31 KiB
Diff
155 lines
31 KiB
Diff
|
|
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);
|
||
|
|
});
|