
# 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 | डिस्क पर कोई अनाथ फ़ाइल नहीं बची (पैच सत्यापित) |