@naturalcycles/backend-lib 9.69.0 → 9.70.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,6 +19,7 @@ function requireSecureHeaderOrAdmin(cfg, reqPermissions) {
19
19
  // Header provided - don't check for Admin
20
20
  if (providedHeader) {
21
21
  if (!secureHeaderValue || timingSafeStringEqual(providedHeader, secureHeaderValue)) {
22
+ req.isAuthenticatedAdminRequest = true;
22
23
  return next();
23
24
  }
24
25
  return next(new AppError('secureHeader or adminToken is required', {
@@ -64,6 +64,7 @@ export function compressionMiddleware(options) {
64
64
  let length;
65
65
  let listeners = [];
66
66
  let stream;
67
+ let closed = false;
67
68
  const _end = res.end;
68
69
  const _on = res.on;
69
70
  const _write = res.write;
@@ -126,6 +127,17 @@ export function compressionMiddleware(options) {
126
127
  addListeners(res, _on, listeners);
127
128
  listeners = null;
128
129
  }
130
+ // Release the compression stream when the response closes, freeing native
131
+ // zlib memory even if the client disconnected before the response finished.
132
+ // Registered before onHeaders so a close that happens before the stream
133
+ // exists is not missed (the `closed` flag below handles a stream created
134
+ // afterwards). Fixes CVE-2026-87776 / GHSA-vc2v-76pw-4v95.
135
+ // We use `stream.destroy()` (not the `destroy` npm package upstream added)
136
+ // because our Node target frees the zlib handle on destroy().
137
+ _on.call(res, 'close', function onResponseClose() {
138
+ closed = true;
139
+ stream?.destroy();
140
+ });
129
141
  onHeaders(res, function onResponseHeaders() {
130
142
  // determine if request is filtered
131
143
  if (!filter(req, res)) {
@@ -184,6 +196,14 @@ export function compressionMiddleware(options) {
184
196
  else {
185
197
  stream = zlib.createDeflate(zlibOpts);
186
198
  }
199
+ // The response already closed before the stream was created, so the close
200
+ // listener above has already run. Release the stream now and drop the
201
+ // reference so later writes fall back to the raw response.
202
+ if (closed) {
203
+ stream.destroy();
204
+ stream = undefined;
205
+ return;
206
+ }
187
207
  // add buffered listeners to stream
188
208
  addListeners(stream, stream.on.bind(stream), listeners);
189
209
  // header fields
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/backend-lib",
3
3
  "type": "module",
4
- "version": "9.69.0",
4
+ "version": "9.70.1",
5
5
  "dependencies": {
6
6
  "@naturalcycles/db-lib": "^10",
7
7
  "@naturalcycles/js-lib": "^15",
@@ -44,6 +44,7 @@ function requireSecureHeaderOrAdmin(
44
44
  // Header provided - don't check for Admin
45
45
  if (providedHeader) {
46
46
  if (!secureHeaderValue || timingSafeStringEqual(providedHeader, secureHeaderValue)) {
47
+ req.isAuthenticatedAdminRequest = true
47
48
  return next()
48
49
  }
49
50
 
@@ -113,6 +113,7 @@ export function compressionMiddleware(options?: CompressionOptions): BackendRequ
113
113
  let length: number | undefined
114
114
  let listeners: [string, (...args: any[]) => void][] | null = []
115
115
  let stream: zlib.Gzip | zlib.Deflate | zlib.BrotliCompress | zlib.ZstdCompress | undefined
116
+ let closed = false
116
117
 
117
118
  const _end = res.end
118
119
  const _on = res.on
@@ -199,6 +200,18 @@ export function compressionMiddleware(options?: CompressionOptions): BackendRequ
199
200
  listeners = null
200
201
  }
201
202
 
203
+ // Release the compression stream when the response closes, freeing native
204
+ // zlib memory even if the client disconnected before the response finished.
205
+ // Registered before onHeaders so a close that happens before the stream
206
+ // exists is not missed (the `closed` flag below handles a stream created
207
+ // afterwards). Fixes CVE-2026-87776 / GHSA-vc2v-76pw-4v95.
208
+ // We use `stream.destroy()` (not the `destroy` npm package upstream added)
209
+ // because our Node target frees the zlib handle on destroy().
210
+ _on.call(res, 'close', function onResponseClose() {
211
+ closed = true
212
+ stream?.destroy()
213
+ })
214
+
202
215
  onHeaders(res, function onResponseHeaders() {
203
216
  // determine if request is filtered
204
217
  if (!filter(req, res)) {
@@ -268,6 +281,15 @@ export function compressionMiddleware(options?: CompressionOptions): BackendRequ
268
281
  stream = zlib.createDeflate(zlibOpts)
269
282
  }
270
283
 
284
+ // The response already closed before the stream was created, so the close
285
+ // listener above has already run. Release the stream now and drop the
286
+ // reference so later writes fall back to the raw response.
287
+ if (closed) {
288
+ stream.destroy()
289
+ stream = undefined
290
+ return
291
+ }
292
+
271
293
  // add buffered listeners to stream
272
294
  addListeners(stream, stream.on.bind(stream), listeners!)
273
295