stream: making DecompressionStream spec compilent for trailing junk

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: https://github.com/nodejs/node/issues/58247
PR-URL: https://github.com/nodejs/node/pull/58316
Reviewed-By: Darshan Sen <raisinten@gmail.com>
Reviewed-By: Matthew Aitken <maitken033380023@gmail.com>
This commit is contained in:
0hm☘️ 2025-05-26 06:08:39 +05:30 committed by GitHub
parent e201299011
commit 62855f3b8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 83 additions and 10 deletions

View File

@ -3014,6 +3014,15 @@ category.
The `node:trace_events` module could not be loaded because Node.js was compiled
with the `--without-v8-platform` flag.
<a id="ERR_TRAILING_JUNK_AFTER_STREAM_END"></a>
### `ERR_TRAILING_JUNK_AFTER_STREAM_END`
Trailing junk found after the end of the compressed stream.
This error is thrown when extra, unexpected data is detected
after the end of a compressed stream (for example, in zlib
or gzip decompression).
<a id="ERR_TRANSFORM_ALREADY_TRANSFORMING"></a>
### `ERR_TRANSFORM_ALREADY_TRANSFORMING`

View File

@ -1803,6 +1803,8 @@ E('ERR_TRACE_EVENTS_CATEGORY_REQUIRED',
'At least one category is required', TypeError);
E('ERR_TRACE_EVENTS_UNAVAILABLE', 'Trace events are unavailable', Error);
E('ERR_TRAILING_JUNK_AFTER_STREAM_END', 'Trailing junk found after the end of the compressed stream', TypeError);
// This should probably be a `RangeError`.
E('ERR_TTY_INIT_FAILED', 'TTY initialization failed', SystemError);
E('ERR_UNAVAILABLE_DURING_EXIT', 'Cannot call function in process exit ' +

View File

@ -99,16 +99,28 @@ class DecompressionStream {
});
switch (format) {
case 'deflate':
this.#handle = lazyZlib().createInflate();
this.#handle = lazyZlib().createInflate({
rejectGarbageAfterEnd: true,
});
break;
case 'deflate-raw':
this.#handle = lazyZlib().createInflateRaw();
break;
case 'gzip':
this.#handle = lazyZlib().createGunzip();
this.#handle = lazyZlib().createGunzip({
rejectGarbageAfterEnd: true,
});
break;
}
this.#transform = newReadableWritablePairFromDuplex(this.#handle);
this.#handle.on('error', (err) => {
if (this.#transform?.writable &&
!this.#transform.writable.locked &&
typeof this.#transform.writable.abort === 'function') {
this.#transform.writable.abort(err);
}
});
}
/**

View File

@ -42,6 +42,7 @@ const {
ERR_BUFFER_TOO_LARGE,
ERR_INVALID_ARG_TYPE,
ERR_OUT_OF_RANGE,
ERR_TRAILING_JUNK_AFTER_STREAM_END,
ERR_ZSTD_INVALID_PARAM,
},
genericNodeError,
@ -266,6 +267,8 @@ function ZlibBase(opts, mode, handle, { flush, finishFlush, fullFlush }) {
this._defaultFullFlushFlag = fullFlush;
this._info = opts?.info;
this._maxOutputLength = maxOutputLength;
this._rejectGarbageAfterEnd = opts?.rejectGarbageAfterEnd === true;
}
ObjectSetPrototypeOf(ZlibBase.prototype, Transform.prototype);
ObjectSetPrototypeOf(ZlibBase, Transform);
@ -570,6 +573,14 @@ function processCallback() {
// stream has ended early.
// This applies to streams where we don't check data past the end of
// what was consumed; that is, everything except Gunzip/Unzip.
if (self._rejectGarbageAfterEnd) {
const err = new ERR_TRAILING_JUNK_AFTER_STREAM_END();
self.destroy(err);
this.cb(err);
return;
}
self.push(null);
}
@ -662,6 +673,7 @@ function Zlib(opts, mode) {
this._level = level;
this._strategy = strategy;
this._mode = mode;
}
ObjectSetPrototypeOf(Zlib.prototype, ZlibBase.prototype);
ObjectSetPrototypeOf(Zlib, ZlibBase);

View File

@ -0,0 +1,46 @@
'use strict';
require('../common');
const assert = require('assert').strict;
const test = require('node:test');
const { DecompressionStream } = require('stream/web');
async function expectTypeError(promise) {
let threw = false;
try {
await promise;
} catch (err) {
threw = true;
assert(err instanceof TypeError, `Expected TypeError, got ${err}`);
}
assert(threw, 'Expected promise to reject');
}
test('DecompressStream deflat emits error on trailing data', async () => {
const valid = new Uint8Array([120, 156, 75, 4, 0, 0, 98, 0, 98]); // deflate('a')
const empty = new Uint8Array(1);
const invalid = new Uint8Array([...valid, ...empty]);
const double = new Uint8Array([...valid, ...valid]);
for (const chunk of [[invalid], [valid, empty], [valid, valid], [valid, double]]) {
await expectTypeError(
Array.fromAsync(
new Blob([chunk]).stream().pipeThrough(new DecompressionStream('deflate'))
)
);
}
});
test('DecompressStream gzip emits error on trailing data', async () => {
const valid = new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 75, 4,
0, 67, 190, 183, 232, 1, 0, 0, 0]); // gzip('a')
const empty = new Uint8Array(1);
const invalid = new Uint8Array([...valid, ...empty]);
const double = new Uint8Array([...valid, ...valid]);
for (const chunk of [[invalid], [valid, empty], [valid, valid], [double]]) {
await expectTypeError(
Array.fromAsync(
new Blob([chunk]).stream().pipeThrough(new DecompressionStream('gzip'))
)
);
}
});

View File

@ -11,14 +11,6 @@
"compression-with-detach.tentative.window.js": {
"requires": ["crypto"]
},
"decompression-corrupt-input.tentative.any.js": {
"fail": {
"expected": [
"trailing junk for 'deflate' should give an error",
"trailing junk for 'gzip' should give an error"
]
}
},
"idlharness-shadowrealm.window.js": {
"skip": "ShadowRealm support is not enabled"
},