Skip to content

Commit 002c166

Browse files
committed
zlib: add error handling for trailing junk after stream end
Introduce `ERR_TRAILING_JUNK_AFTER_STREAM_END` error to handle unexpected data after the end of a compressed stream. This ensures proper error reporting when decompressing deflate or gzip streams with trailing junk. Added tests to verify the behavior. Fixes: #58247
1 parent dc7ec42 commit 002c166

File tree

5 files changed

+81
-2
lines changed

5 files changed

+81
-2
lines changed

doc/api/errors.md

+7
Original file line numberDiff line numberDiff line change
@@ -3012,6 +3012,13 @@ category.
30123012
The `node:trace_events` module could not be loaded because Node.js was compiled
30133013
with the `--without-v8-platform` flag.
30143014

3015+
<a id="ERR_TRAILING_JUNK_AFTER_STREAM_END"></a>
3016+
3017+
### `ERR_TRAILING_JUNK_AFTER_STREAM_END`
3018+
3019+
Trailing junk found after the end of the compressed stream.
3020+
This error is thrown when extra, unexpected data is detected after the end of a compressed stream (for example, in zlib or gzip decompression).
3021+
30153022
<a id="ERR_TRANSFORM_ALREADY_TRANSFORMING"></a>
30163023

30173024
### `ERR_TRANSFORM_ALREADY_TRANSFORMING`

lib/internal/errors.js

+2
Original file line numberDiff line numberDiff line change
@@ -1803,6 +1803,8 @@ E('ERR_TRACE_EVENTS_CATEGORY_REQUIRED',
18031803
'At least one category is required', TypeError);
18041804
E('ERR_TRACE_EVENTS_UNAVAILABLE', 'Trace events are unavailable', Error);
18051805

1806+
E('ERR_TRAILING_JUNK_AFTER_STREAM_END', 'Trailing junk found after the end of the compressed stream', TypeError);
1807+
18061808
// This should probably be a `RangeError`.
18071809
E('ERR_TTY_INIT_FAILED', 'TTY initialization failed', SystemError);
18081810
E('ERR_UNAVAILABLE_DURING_EXIT', 'Cannot call function in process exit ' +

lib/internal/webstreams/compression.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,28 @@ class DecompressionStream {
9999
});
100100
switch (format) {
101101
case 'deflate':
102-
this.#handle = lazyZlib().createInflate();
102+
this.#handle = lazyZlib().createInflate({
103+
rejectGarbageAfterEnd: true,
104+
});
103105
break;
104106
case 'deflate-raw':
105107
this.#handle = lazyZlib().createInflateRaw();
106108
break;
107109
case 'gzip':
108-
this.#handle = lazyZlib().createGunzip();
110+
this.#handle = lazyZlib().createGunzip({
111+
rejectGarbageAfterEnd: true,
112+
});
109113
break;
110114
}
111115
this.#transform = newReadableWritablePairFromDuplex(this.#handle);
116+
117+
this.#handle.on('error', (err) => {
118+
if (this.#transform?.writable &&
119+
!this.#transform.writable.locked &&
120+
typeof this.#transform.writable.abort === 'function') {
121+
this.#transform.writable.abort(err);
122+
}
123+
});
112124
}
113125

114126
/**

lib/zlib.js

+12
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const {
4242
ERR_BUFFER_TOO_LARGE,
4343
ERR_INVALID_ARG_TYPE,
4444
ERR_OUT_OF_RANGE,
45+
ERR_TRAILING_JUNK_AFTER_STREAM_END,
4546
ERR_ZSTD_INVALID_PARAM,
4647
},
4748
genericNodeError,
@@ -266,6 +267,8 @@ function ZlibBase(opts, mode, handle, { flush, finishFlush, fullFlush }) {
266267
this._defaultFullFlushFlag = fullFlush;
267268
this._info = opts?.info;
268269
this._maxOutputLength = maxOutputLength;
270+
271+
this._rejectGarbageAfterEnd = opts?.rejectGarbageAfterEnd === true;
269272
}
270273
ObjectSetPrototypeOf(ZlibBase.prototype, Transform.prototype);
271274
ObjectSetPrototypeOf(ZlibBase, Transform);
@@ -570,6 +573,14 @@ function processCallback() {
570573
// stream has ended early.
571574
// This applies to streams where we don't check data past the end of
572575
// what was consumed; that is, everything except Gunzip/Unzip.
576+
577+
if (self._rejectGarbageAfterEnd) {
578+
const err = new ERR_TRAILING_JUNK_AFTER_STREAM_END();
579+
self.destroy(err);
580+
this.cb(err);
581+
return;
582+
}
583+
573584
self.push(null);
574585
}
575586

@@ -662,6 +673,7 @@ function Zlib(opts, mode) {
662673

663674
this._level = level;
664675
this._strategy = strategy;
676+
this._mode = mode;
665677
}
666678
ObjectSetPrototypeOf(Zlib.prototype, ZlibBase.prototype);
667679
ObjectSetPrototypeOf(Zlib, ZlibBase);

test/parallel/test-zlib-type-error.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use strict';
2+
require('../common');
3+
const assert = require('assert').strict;
4+
const test = require('node:test');
5+
const { DecompressionStream } = require('stream/web');
6+
7+
async function expectTypeError(promise) {
8+
let threw = false;
9+
try {
10+
await promise;
11+
} catch (err) {
12+
threw = true;
13+
assert(err instanceof TypeError, `Expected TypeError, got ${err}`);
14+
}
15+
assert(threw, 'Expected promise to reject');
16+
}
17+
18+
test('DecompressStream deflat emits error on trailing data', async () => {
19+
const valid = new Uint8Array([120, 156, 75, 4, 0, 0, 98, 0, 98]); // deflate('a')
20+
const empty = new Uint8Array(1);
21+
const invalid = new Uint8Array([...valid, ...empty]);
22+
const double = new Uint8Array([...valid, ...valid]);
23+
24+
for (const chunk of [[invalid], [valid, empty], [valid, valid], [valid, double]]) {
25+
await expectTypeError(
26+
Array.fromAsync(
27+
new Blob([chunk]).stream().pipeThrough(new DecompressionStream('deflate'))
28+
)
29+
);
30+
}
31+
});
32+
33+
test('DecompressStream gzip emits error on trailing data', async () => {
34+
const valid = new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 75, 4,
35+
0, 67, 190, 183, 232, 1, 0, 0, 0]); // gzip('a')
36+
const empty = new Uint8Array(1);
37+
const invalid = new Uint8Array([...valid, ...empty]);
38+
const double = new Uint8Array([...valid, ...valid]);
39+
for (const chunk of [[invalid], [valid, empty], [valid, valid], [double]]) {
40+
await expectTypeError(
41+
Array.fromAsync(
42+
new Blob([chunk]).stream().pipeThrough(new DecompressionStream('gzip'))
43+
)
44+
);
45+
}
46+
});

0 commit comments

Comments
 (0)