@@ -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+
10101158function createHttp2Server ( opts , fn ) {
10111159 var _compression = compression ( opts )
10121160 var server = http2 . createServer ( function ( req , res ) {
0 commit comments