Skip to content
KitploitKITPLOIT
ToolsBlog
Log in
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-3304 — Reproduction lab for CVE-2026-3304, a Multer async fileFilter race condition causing disk exhaustion via orphaned temp files. Includes vulnerable and patched Docker servers, exploit scripts, and root cause analysis. | Kitploit
Tools/GitHubGitHub/mkway/cve-2026-3304
Vulnerability AnalysisExploitationWeb SecurityLearning & EducationLabs & Practice
GitHubmkway/cve-2026-3304

CVE-2026-3304

Reproduction lab for CVE-2026-3304, a Multer async fileFilter race condition causing disk exhaustion via orphaned temp files. Includes vulnerable and patched Docker servers, exploit scripts, and root cause analysis.

View Repository
217 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-3304 Lab Environment

For educational and research purposes only. Attacking systems without authorization is illegal.

This lab environment was built by analyzing the Multer 2.1.0 patch and its official test code. The vulnerable server replicates the exact conditions described in the patch test, and the patched server runs Multer 2.1.0 to confirm the fix works.

Vulnerability Overview

FieldDetails
CVECVE-2026-3304
TargetMulter < 2.1.0 (Node.js multipart/form-data middleware)
TypeDoS — Orphaned File (incomplete temporary file cleanup)
CWECWE-459: Incomplete Cleanup
CVSS 4.08.7 HIGH
Patched VersionMulter 2.1.0

A malformed multipart request with a missing name attribute on a file part causes Multer to create a temporary file on disk but never clean it up. Repeated requests exhaust disk space, resulting in a Denial of Service.


Root Cause Analysis

The vulnerability originates in the fileFilter callback handling flow inside multer/lib/make-middleware.js.

Core problem: setImmediate timing + missing errorOccured check

When Multer streams and parses a multipart request part by part, the following sequence occurs:

[Parsing event sequence — vulnerable version]

1. Part 1 header received
   → fileFilter(req, file, cb) called
   → setImmediate(cb) → callback deferred to next event loop tick

2. Part 1 body received
   → /tmp/uploads/<uuid> opened, data starts being written

3. Part 2 header received (name attribute missing)
   → Multer detects 'name missing'
   → errorOccured = true  ← error flag set
   → abortWithCode('LIMIT_FIELD_KEY') called → HTTP 500 scheduled

4. setImmediate callback fires (next event loop tick)
   → fileFilter result: includeFile = true (normal flow)
   → [BUG] errorOccured flag is NOT checked
   → storage._handleFile() called → temp file committed to disk

5. HTTP 500 response sent
   → temp file remains on disk (orphaned file)

Vulnerable code (make-middleware.js — Multer < 2.1.0)

// fileFilter completion callback (deferred via setImmediate)
fileFilter(req, file, function (err, includeFile) {
  if (err) {
    appender.removePlaceholder(placeholder)
    return abortWithError(uploadedFiles, err)
  }

  if (!includeFile) {
    appender.removePlaceholder(placeholder)
    return fileStream.resume()
  }

  // ❌ errorOccured is never checked here
  //    even if an error was set while parsing Part 2, execution continues
  storage._handleFile(req, file, function (err, info) {
    if (err) {
      appender.removePlaceholder(placeholder)
      return abortWithError(uploadedFiles, err)
    }
    // temp file is registered in uploadedFiles and left on disk
    appender.replacePlaceholder(placeholder, assign(file, info))
    checkFinished()
  })
})

Why is setImmediate the problem?

Wrapping fileFilter with setImmediate defers its callback to the next event loop tick. In that window, busboy (the multipart parser) continues parsing the next part's headers, discovers the missing name, and sets errorOccured = true. When the callback resumes, the error state is already set — but the code never checks it, so storage._handleFile is called unconditionally and the temp file is written to disk.


Patch Code Analysis (Multer 2.1.0)

Fix commit: 739919097d

A single if (errorOccured) guard was added immediately after the fileFilter callback entry, before storage._handleFile is reached.

// fileFilter completion callback (Multer 2.1.0)
fileFilter(req, file, function (err, includeFile) {
  if (err) {
    appender.removePlaceholder(placeholder)
    return abortWithError(uploadedFiles, err)
  }

  // ✅ [PATCH] Check errorOccured before proceeding
  if (errorOccured) {
    appender.removePlaceholder(placeholder)
    return fileStream.resume()   // drain stream — no file written to disk
  }

  if (!includeFile) {
    appender.removePlaceholder(placeholder)
    return fileStream.resume()
  }

  storage._handleFile(req, file, function (err, info) {
    if (err) {
      appender.removePlaceholder(placeholder)
      return abortWithError(uploadedFiles, err)
    }
    appender.replacePlaceholder(placeholder, assign(file, info))
    checkFinished()
  })
})

Summary of changes

ItemVulnerable (< 2.1.0)Patched (2.1.0)
errorOccured check❌ Not checked✅ Checked immediately on callback entry
_handleFile called on errorYesBlocked
Temp file cleanup❌ Missing✅ Via fileStream.resume()
Orphaned files per request10

Why fileStream.resume() cleans up: Calling fileStream.resume() drains and discards the stream without passing it to DiskStorage, so no file is written and nothing is left on disk.


Official Patch Test Code

The Multer team included the following Mocha test in the 2.1.0 patch to verify the fix. This test became the blueprint for this lab environment — the vulnerable server replicates the exact setup described here (setImmediate fileFilter + malformed multipart request), and the expected behavior is verified against both the vulnerable and patched versions.

/* eslint-env mocha */

var assert = require('assert')
var fs = require('fs')
var os = require('os')
var path = require('path')
var http = require('http')

var express = require('express')
var multer = require('../')

describe('async fileFilter cleanup', function () {
  it('does not leave orphan files when request aborts with missing field name', function (done) {
    var uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'multer-orphan-'))
    var app = express()

    // Vulnerability trigger: async fileFilter via setImmediate
    var upload = multer({
      dest: uploadDir,
      fileFilter: function (req, file, cb) {
        setImmediate(function () { cb(null, true) })
      }
    })

    app.post('/upload', upload.any(), function (req, res) {
      res.json({ success: true })
    })

    // Error handler: respond with 400
    app.use(function (err, req, res, next) {
      res.status(400).json({ error: err.code })
    })

    var server = app.listen(0, function () {
      var port = server.address().port
      var boundary = 'TestBound'

      // Malicious body: Part 1 valid, Part 2 missing name attribute
      var body =
        '--' + boundary + '\r\n' +
        'Content-Disposition: form-data; name="f"; filename="a.bin"\r\n' +
        'Content-Type: application/octet-stream\r\n\r\nORPHAN FILE DATA\r\n' +
        '--' + boundary + '\r\n' +
        'Content-Disposition: form-data; filename="b.bin"\r\n' +   // ← name= missing
        'Content-Type: application/octet-stream\r\n\r\nx\r\n' +
        '--' + boundary + '--\r\n'

      var req = http.request({
        hostname: 'localhost',
        port: port,
        path: '/upload',
        method: 'POST',
        headers: {
          'Content-Type': 'multipart/form-data; boundary=' + boundary,
          'Content-Length': Buffer.byteLength(body)
        }
      }, function (res) {
        res.resume()
        res.on('end', function () {
          setTimeout(function () {
            var files = fs.readdirSync(uploadDir)

            // Assert 1: server must respond with 400 (error)
            assert.strictEqual(res.statusCode, 400)

            // Assert 2: no orphaned files must remain on disk
            assert.strictEqual(files.length, 0)

            server.close(done)
          }, 500)
        })
      })

      req.write(body)
      req.end()
    })
  })
})
AssertMeaning
res.statusCode === 400Multer correctly rejects the malformed request
files.length === 0No orphaned files left on disk (patch verified)
Download Tool