Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions doc/api/zlib.md
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,9 @@ These advanced options are available for controlling decompression:
<!-- YAML
added: v0.11.1
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/64023
description: The `rejectGarbageAfterEnd` option was added.
- version:
- v14.5.0
- v12.19.0
Expand Down Expand Up @@ -836,6 +839,10 @@ ignored by the decompression classes.
* `info` {boolean} (If `true`, returns an object with `buffer` and `engine`.)
* `maxOutputLength` {integer} Limits output size when using
[convenience methods][]. **Default:** [`buffer.kMaxLength`][]
* `rejectGarbageAfterEnd` {boolean} If `true`, decompression fails when
trailing input is detected after the end of the compressed stream. This
includes unreadable bytes and, when decompressing gzip, additional gzip
members following the first member. **Default:** `false`

See the [`deflateInit2` and `inflateInit2`][] documentation for more
information.
Expand All @@ -845,6 +852,9 @@ information.
<!-- YAML
added: v11.7.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/64023
description: The `rejectGarbageAfterEnd` option was added.
- version:
- v14.5.0
- v12.19.0
Expand All @@ -863,6 +873,8 @@ Each Brotli-based class takes an `options` object. All options are optional.
* `maxOutputLength` {integer} Limits output size when using
[convenience methods][]. **Default:** [`buffer.kMaxLength`][]
* `info` {boolean} If `true`, returns an object with `buffer` and `engine`. **Default:** `false`
* `rejectGarbageAfterEnd` {boolean} If `true`, decompression fails when
input remains after the first complete compressed stream. **Default:** `false`

For example:

Expand Down Expand Up @@ -1086,6 +1098,10 @@ the inflate and deflate algorithms.
added:
- v23.8.0
- v22.15.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/64023
description: The `rejectGarbageAfterEnd` option was added.
-->

<!--type=misc-->
Expand All @@ -1102,6 +1118,8 @@ Each Zstd-based class takes an `options` object. All options are optional.
* `dictionary` {Buffer} Optional dictionary used to
improve compression efficiency when compressing or decompressing data that
shares common patterns with the dictionary.
* `rejectGarbageAfterEnd` {boolean} If `true`, decompression fails when
input remains after the first complete compressed stream. **Default:** `false`

For example:

Expand Down
16 changes: 15 additions & 1 deletion lib/zlib.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const {
const { owner_symbol } = require('internal/async_hooks').symbols;
const {
checkRangesOrGetDefault,
validateBoolean,
validateFunction,
validateUint32,
validateFiniteNumber,
Expand Down Expand Up @@ -246,6 +247,13 @@ function ZlibBase(opts, mode, handle, { flush, finishFlush, fullFlush }) {
opts.maxOutputLength, 'options.maxOutputLength',
1, kMaxLength, kMaxLength);

if (opts.rejectGarbageAfterEnd !== undefined) {
validateBoolean(
opts.rejectGarbageAfterEnd,
'options.rejectGarbageAfterEnd',
);
}

if (opts.encoding || opts.objectMode || opts.writableObjectMode) {
opts = { ...opts };
opts.encoding = null;
Expand Down Expand Up @@ -472,6 +480,11 @@ function processChunkSync(self, chunk, flushFlag) {
}
}

if (availInAfter > 0 && self._rejectGarbageAfterEnd) {
_close(self);
throw new ERR_TRAILING_JUNK_AFTER_STREAM_END();
}

self.bytesWritten = inputRead;
_close(self);

Expand Down Expand Up @@ -678,7 +691,8 @@ function Zlib(opts, mode) {
strategy,
this._writeState,
processCallback,
dictionary);
dictionary,
opts?.rejectGarbageAfterEnd === true);

ZlibBase.call(this, opts, mode, handle, zlibDefaultOpts);

Expand Down
37 changes: 26 additions & 11 deletions src/node_zlib.cc
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ class ZlibContext final : public MemoryRetainer {
int window_bits,
int mem_level,
int strategy,
bool reject_garbage_after_end,
std::vector<unsigned char>&& dictionary);
CompressionError SetParams(int level, int strategy);

Expand Down Expand Up @@ -223,6 +224,7 @@ class ZlibContext final : public MemoryRetainer {
node_zlib_mode mode_ = NONE;
int strategy_ = 0;
int window_bits_ = 0;
bool reject_garbage_after_end_ = false;
unsigned int gzip_id_bytes_read_ = 0;
std::vector<unsigned char> dictionary_;

Expand Down Expand Up @@ -749,9 +751,10 @@ class ZlibStream final : public CompressionStream<ZlibContext> {
"a version of npm (> 5.5.1 or < 5.4.0) or node-tar (> 4.0.1) "
"that is compatible with Node.js 9 and above.\n");
}
CHECK(args.Length() == 7 &&
"init(windowBits, level, memLevel, strategy, writeResult, writeCallback,"
" dictionary)");
CHECK((args.Length() == 7 || args.Length() == 8) &&
"init(windowBits, level, memLevel, strategy, writeResult, "
"writeCallback,"
" dictionary[, rejectGarbageAfterEnd])");

ZlibStream* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This());
Expand Down Expand Up @@ -791,10 +794,20 @@ class ZlibStream final : public CompressionStream<ZlibContext> {
data + Buffer::Length(args[6]));
}

bool reject_garbage_after_end = false;
if (args.Length() == 8) {
CHECK(args[7]->IsBoolean());
reject_garbage_after_end = args[7]->IsTrue();
}

wrap->InitStream(write_result, write_js_callback);

AllocScope alloc_scope(wrap);
wrap->context()->Init(level, window_bits, mem_level, strategy,
wrap->context()->Init(level,
window_bits,
mem_level,
strategy,
reject_garbage_after_end,
std::move(dictionary));
}

Expand Down Expand Up @@ -1124,10 +1137,8 @@ void ZlibContext::DoThreadPoolWork() {
}
}

while (strm_.avail_in > 0 &&
mode_ == GUNZIP &&
err_ == Z_STREAM_END &&
strm_.next_in[0] != 0x00) {
while (strm_.avail_in > 0 && mode_ == GUNZIP && err_ == Z_STREAM_END &&
!reject_garbage_after_end_ && strm_.next_in[0] != 0x00) {
// Bytes remain in input buffer. Perhaps this is another compressed
// member in the same archive, or just trailing garbage.
// Trailing zero bytes are okay, though, since they are frequently
Expand Down Expand Up @@ -1226,9 +1237,12 @@ CompressionError ZlibContext::ResetStream() {
return SetDictionary();
}

void ZlibContext::Init(
int level, int window_bits, int mem_level, int strategy,
std::vector<unsigned char>&& dictionary) {
void ZlibContext::Init(int level,
int window_bits,
int mem_level,
int strategy,
bool reject_garbage_after_end,
std::vector<unsigned char>&& dictionary) {
// Set allocation functions
strm_.zalloc = CompressionStreamMemoryOwner::AllocForZlib;
strm_.zfree = CompressionStreamMemoryOwner::FreeForZlib;
Expand Down Expand Up @@ -1259,6 +1273,7 @@ void ZlibContext::Init(
window_bits_ = window_bits;
mem_level_ = mem_level;
strategy_ = strategy;
reject_garbage_after_end_ = reject_garbage_after_end;

flush_ = Z_NO_FLUSH;

Expand Down
144 changes: 144 additions & 0 deletions test/parallel/test-zlib-reject-garbage-after-end.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
'use strict';

require('../common');
const assert = require('assert');
const test = require('node:test');
const { finished } = require('stream/promises');
const zlib = require('zlib');

const trailingJunkError = {
code: 'ERR_TRAILING_JUNK_AFTER_STREAM_END',
name: 'TypeError',
};

function callAsync(fn, input, options) {
return new Promise((resolve, reject) => {
fn(input, options, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}

async function collect(stream, input) {
const chunks = [];
stream.on('data', (chunk) => chunks.push(chunk));
stream.end(input);
await finished(stream);
return Buffer.concat(chunks);
}

const cases = [
{
label: 'inflate',
compress: zlib.deflateSync,
decompress: zlib.inflate,
decompressSync: zlib.inflateSync,
createDecompress: zlib.createInflate,
defaultOutput: 'a',
},
{
label: 'inflateRaw',
compress: zlib.deflateRawSync,
decompress: zlib.inflateRaw,
decompressSync: zlib.inflateRawSync,
createDecompress: zlib.createInflateRaw,
defaultOutput: 'a',
},
{
label: 'gunzip',
compress: zlib.gzipSync,
decompress: zlib.gunzip,
decompressSync: zlib.gunzipSync,
createDecompress: zlib.createGunzip,
defaultOutput: 'aa',
},
{
label: 'unzip',
compress: zlib.gzipSync,
decompress: zlib.unzip,
decompressSync: zlib.unzipSync,
createDecompress: zlib.createUnzip,
defaultOutput: 'aa',
},
{
label: 'brotli',
compress: zlib.brotliCompressSync,
decompress: zlib.brotliDecompress,
decompressSync: zlib.brotliDecompressSync,
createDecompress: zlib.createBrotliDecompress,
defaultOutput: 'a',
},
{
label: 'zstd',
compress: zlib.zstdCompressSync,
decompress: zlib.zstdDecompress,
decompressSync: zlib.zstdDecompressSync,
createDecompress: zlib.createZstdDecompress,
defaultOutput: 'a',
},
];

for (const {
label,
compress,
decompress,
decompressSync,
createDecompress,
defaultOutput,
} of cases) {
test(`rejectGarbageAfterEnd rejects trailing input for ${label}`, async () => {
const compressed = compress(Buffer.from('a'));
const withTrailingInput = Buffer.concat([compressed, compressed]);

assert.strictEqual(decompressSync(withTrailingInput).toString(), defaultOutput);
assert.strictEqual(
(await callAsync(decompress, withTrailingInput)).toString(),
defaultOutput,
);
assert.strictEqual(
(await collect(createDecompress(), withTrailingInput)).toString(),
defaultOutput,
);

assert.throws(
() => decompressSync(withTrailingInput, { rejectGarbageAfterEnd: true }),
trailingJunkError,
);
await assert.rejects(
callAsync(decompress, withTrailingInput, { rejectGarbageAfterEnd: true }),
trailingJunkError,
);
await assert.rejects(
collect(
createDecompress({ rejectGarbageAfterEnd: true }),
withTrailingInput,
),
trailingJunkError,
);
});
}

test('rejectGarbageAfterEnd must be a boolean', () => {
const compressed = zlib.deflateSync(Buffer.from('a'));

for (const value of [1, 'true', null]) {
assert.throws(
() => zlib.inflateSync(compressed, { rejectGarbageAfterEnd: value }),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
},
);
assert.throws(
() => zlib.createInflate({ rejectGarbageAfterEnd: value }),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
},
);
}
});
Loading
Loading