Skip to content

Commit 6a6b043

Browse files
committed
Merge branch 'master' into release/1.8.2
2 parents df3127c + 151f63e commit 6a6b043

4 files changed

Lines changed: 173 additions & 0 deletions

File tree

HISTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
1.8.2
22
=====
3+
34
* Fix [CVE-2026-87776](https://www.cve.org/CVERecord?id=CVE-2026-87776) ([GHSA-vc2v-76pw-4v95](https://github.com/expressjs/compression/security/advisories/GHSA-vc2v-76pw-4v95))
5+
* deps: add destroy@1.2.0
46
* Match `Cache-Control: no-transform` directive case-insensitively
57

68
1.8.1

index.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ var Buffer = require('safe-buffer').Buffer
1919
var bytes = require('bytes')
2020
var compressible = require('compressible')
2121
var debug = require('debug')('compression')
22+
var destroy = require('destroy')
2223
var onHeaders = require('on-headers')
2324
var vary = require('vary')
2425
var zlib = require('zlib')
@@ -82,6 +83,7 @@ function compression (options) {
8283
var length
8384
var listeners = []
8485
var stream
86+
var closed = false
8587

8688
var _end = res.end
8789
var _on = res.on
@@ -158,6 +160,17 @@ function compression (options) {
158160
listeners = null
159161
}
160162

163+
// Release the compression stream when the response closes, freeing native
164+
// zlib resources even if the client disconnected before it finished.
165+
// Registered before onHeaders so a close before the stream exists is not
166+
// missed; destroy() no-ops safely when no stream was created, and is used
167+
// rather than stream.destroy() because destroy() alone leaks the zlib
168+
// handle on some Node.js versions.
169+
_on.call(res, 'close', function onResponseClose () {
170+
closed = true
171+
destroy(stream)
172+
})
173+
161174
onHeaders(res, function onResponseHeaders () {
162175
// determine if request is filtered
163176
if (!filter(req, res)) {
@@ -217,6 +230,15 @@ function compression (options) {
217230
? zlib.createBrotliCompress(optsBrotli)
218231
: zlib.createDeflate(opts)
219232

233+
// the response already closed before the stream was created, so the
234+
// close listener above has already run. Release the stream now and drop
235+
// the reference so later writes fall back to the raw response.
236+
if (closed) {
237+
destroy(stream)
238+
stream = null
239+
return
240+
}
241+
220242
// add buffered listeners to stream
221243
addListeners(stream, stream.on, listeners)
222244

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"bytes": "3.1.2",
1818
"compressible": "~2.0.18",
1919
"debug": "2.6.9",
20+
"destroy": "1.2.0",
2021
"negotiator": "~0.6.4",
2122
"on-headers": "~1.1.0",
2223
"safe-buffer": "5.2.1",

test/compression.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,74 @@ describe('compression()', function () {
173173
.end(function () {})
174174
})
175175

176+
// @see https://github.com/expressjs/compression/security/advisories/GHSA-vc2v-76pw-4v95
177+
it('should destroy the compression stream on response close (gzip)', function (done) {
178+
assertDestroyedOnClose('createGzip', 'gzip', done)
179+
})
180+
181+
// @see https://github.com/expressjs/compression/security/advisories/GHSA-vc2v-76pw-4v95
182+
it('should destroy the compression stream on response close (deflate)', function (done) {
183+
assertDestroyedOnClose('createDeflate', 'deflate', done)
184+
})
185+
186+
// @see https://github.com/expressjs/compression/security/advisories/GHSA-vc2v-76pw-4v95
187+
brotli('should destroy the compression stream on response close (brotli)', function (done) {
188+
assertDestroyedOnClose('createBrotliCompress', 'br', done)
189+
})
190+
191+
// @see https://github.com/expressjs/compression/security/advisories/GHSA-vc2v-76pw-4v95
192+
it('should destroy a compression stream created after the response closed', function (done) {
193+
var original = zlib.createGzip
194+
var stream
195+
var cleaned = false
196+
// finish once both the server-side assertion has run and the aborted
197+
// request has errored; supertest closes the server after the latter
198+
var cb = after(2, done)
199+
200+
Object.defineProperty(zlib, 'createGzip', {
201+
configurable: true,
202+
value: function () {
203+
stream = original.apply(this, arguments)
204+
wrapCleanup(stream, function () {
205+
cleaned = true
206+
})
207+
return stream
208+
}
209+
})
210+
211+
var server = createServer({ threshold: 0 }, function (req, res) {
212+
res.setHeader('Content-Type', 'text/plain')
213+
res.once('close', function () {
214+
// start an eligible compressed response after the client already left,
215+
// so the stream is created after the only close event
216+
res.write(Buffer.alloc(128 * 1024))
217+
res.end()
218+
setImmediate(function () {
219+
var err = null
220+
try {
221+
assert.ok(stream)
222+
assert.strictEqual(cleaned, true)
223+
} catch (e) {
224+
err = e
225+
}
226+
Object.defineProperty(zlib, 'createGzip', {
227+
configurable: true,
228+
value: original
229+
})
230+
cb(err)
231+
})
232+
})
233+
res.destroy()
234+
})
235+
236+
request(server)
237+
.get('/')
238+
.set('Accept-Encoding', 'gzip')
239+
.end(function () {
240+
cb()
241+
})
242+
})
243+
176244
it('should back-pressure when compressed', function (done) {
177245
var buf
178246
var cb = after(2, done)
@@ -1007,6 +1075,86 @@ function createServer (opts, fn) {
10071075
})
10081076
}
10091077

1078+
// Assert that the compression stream is destroyed when the response closes
1079+
// prematurely (client disconnects mid-response), so the native zlib resources
1080+
// are released. The stream is captured by wrapping the zlib factory, as its
1081+
// destroyed state is not observable from the client.
1082+
// @see https://github.com/expressjs/compression/security/advisories/GHSA-vc2v-76pw-4v95
1083+
function assertDestroyedOnClose (createName, acceptEncoding, done) {
1084+
var original = zlib[createName]
1085+
var stream
1086+
var cleaned = false
1087+
1088+
Object.defineProperty(zlib, createName, {
1089+
configurable: true,
1090+
value: function () {
1091+
stream = original.apply(this, arguments)
1092+
// Record whichever cleanup method the middleware invokes, so the
1093+
// assertion holds on old Node.js versions that expose close() but not
1094+
// destroy()/destroyed.
1095+
wrapCleanup(stream, function () {
1096+
cleaned = true
1097+
})
1098+
return stream
1099+
}
1100+
})
1101+
1102+
var server = createServer({ threshold: 0 }, function (req, res) {
1103+
res.setHeader('Content-Type', 'text/plain')
1104+
var timer = setInterval(function () {
1105+
res.write(Buffer.alloc(128 * 1024))
1106+
}, 5)
1107+
res.once('close', function () {
1108+
clearInterval(timer)
1109+
setImmediate(function () {
1110+
var err = null
1111+
try {
1112+
assert.ok(stream)
1113+
assert.strictEqual(cleaned, true)
1114+
} catch (e) {
1115+
err = e
1116+
}
1117+
Object.defineProperty(zlib, createName, {
1118+
configurable: true,
1119+
value: original
1120+
})
1121+
server.close(function () {
1122+
done(err)
1123+
})
1124+
})
1125+
})
1126+
})
1127+
1128+
request(server)
1129+
.get('/')
1130+
.set('Accept-Encoding', acceptEncoding)
1131+
.request()
1132+
.on('response', function (res) {
1133+
res.once('data', function () {
1134+
res.destroy()
1135+
})
1136+
})
1137+
.on('error', function () {})
1138+
.end()
1139+
}
1140+
1141+
// Wrap the stream's cleanup methods so a test can observe that the middleware
1142+
// released it. destroy() exists from Node.js 8.0.0; older versions (0.8 to
1143+
// 7.x) expose close() instead, so wrap whichever is present.
1144+
function wrapCleanup (stream, onCleanup) {
1145+
var methods = ['destroy', 'close']
1146+
methods.forEach(function (name) {
1147+
var original = stream[name]
1148+
if (typeof original !== 'function') {
1149+
return
1150+
}
1151+
stream[name] = function () {
1152+
onCleanup()
1153+
return original.apply(this, arguments)
1154+
}
1155+
})
1156+
}
1157+
10101158
function createHttp2Server (opts, fn) {
10111159
var _compression = compression(opts)
10121160
var server = http2.createServer(function (req, res) {

0 commit comments

Comments
 (0)