Like a restaurant that never buses its tables, multer keeps serving until there's no room left
CVE-2026-88932 is a denial-of-service flaw in multer, the dominant Express.js middleware for multipart file uploads (~8–20 million weekly npm downloads). In versions 2.2.0 through 2.3.x, when a client aborts an upload *before* the diskStorage engine assigns a destination path, the abort-cleanup routine skips the file because it has no path to delete — but the underlying fs.WriteStream finishes anyway, landing a complete orphan on disk. The bug is a bypass of the earlier CVE-2026-5038 fix (which only cleaned files that already had a path). An unauthenticated remote attacker can repeatedly start and abort multipart POSTs to accumulate orphans until the upload directory or /tmp fills, starving the host of disk. Only diskStorage is affected; memoryStorage and cloud-backed engines are immune. Fixed in multer 2.4.0 via commit 53337f9, which tracks an abortCleanupDone flag and calls storage._removeFile on any late-completing write.
The vendor rates this MEDIUM / 5.3 and that is accurate. The CVSS vector (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L) correctly reflects unauthenticated network access with low availability impact. Upgrading A:L → A:H would require arguing the attack reliably crashes the entire host, but in practice disk-fill is gradual, monitorable, and recoverable — any alerting on disk utilisation catches it long before total outage. The narrow vulnerable version window (two minor releases), the DoS-only impact ceiling, zero in-the-wild exploitation, and available compensating controls (disk quotas, rate-limiting, monitoring) all confirm the vendor's MEDIUM is neither inflated nor understated.
4 steps from start to impact.
Identify multer diskStorage endpoint
multipart/form-data uploads backed by multer's diskStorage engine. This is common on user-facing upload forms, avatar endpoints, and document-ingestion APIs. The route must be reachable without authentication (or with trivially obtainable credentials). No specialised tooling is required — a simple curl with --form suffices.- Target application uses multer 2.2.0–2.3.x with diskStorage
- Upload endpoint is network-reachable
- Many production deployments front uploads behind authentication or CAPTCHA
- Some deployments use memoryStorage, S3 SDKs, or cloud-backed engines — all immune
- The vulnerable version window is only two minor releases
Send multipart upload and abort before path assignment
destination/filename callback returns. If those callbacks are asynchronous (e.g., they hash or query a DB), the race window widens significantly. The abort fires multer's cleanup, which iterates only files that already have a path — the in-flight file has none yet, so it is skipped.- Attacker can open and close TCP connections rapidly
- diskStorage destination/filename callback takes non-trivial time (async I/O widens the window)
- Synchronous callbacks narrow the window to microseconds — the race becomes unreliable
- WAFs and reverse proxies may enforce minimum request durations or rate-limit aborted connections
- CDNs like Cloudflare buffer uploads, so the abort may never reach the origin in the critical window
fs.open / inode creation visible in host-level audit or eBPF tracing.Late-completing write lands orphaned file
fs.WriteStream pipe completes after abort cleanup has already finished. Because abortCleanupDone is true but no code removes the newly written file, it persists on disk with no reference in multer's state. The attacker repeats steps 2–3 at scale to accumulate orphans.- The fs.WriteStream must complete after cleanup (race won)
- Disk must have remaining capacity to accept writes
- OS-level disk quotas on the upload user/directory cap the damage
- Container environments often have ephemeral storage limits (e.g., Kubernetes
emptyDir.sizeLimit) - tmpwatch / systemd-tmpfiles can periodically purge stale files from
/tmp
Disk exhaustion → denial of service
/tmp is full, the application (and potentially co-located services) can no longer write to disk. Log rotation fails, database WAL writes fail, and the Node.js process may crash on ENOSPC errors. The impact is availability loss for the application, not data exfiltration or code execution.- Attacker sustains the abort loop long enough to fill available disk
- No disk monitoring or quota intervenes
- Any disk monitoring with auto-remediation (alert + cleanup script) breaks the chain
- Separate volume mounts for uploads isolate blast radius to the upload feature only
- Kubernetes liveness probes restart the pod; new ephemeral storage resets the attack
df -h on the target host shows 100% utilisation on the upload mount.53337f9 adds abortCleanupDone tracking and calls storage._removeFile on any late-completing write, eliminating orphan accumulation entirely. Since this is an npm dependency, the upgrade is npm install [email protected] and a lockfile commit. No mitigation SLA applies at MEDIUM — go straight to the 365-day noisgate remediation SLA window, though the upgrade is trivial and should be prioritised in your next dependency-update cycle./tmp if that's where multer writes) on its own filesystem or volume with a hard size limit (e.g., fallocate + mkfs + mount, or Kubernetes emptyDir.sizeLimit). When the volume fills, only uploads fail — the application, database, and logs continue on the root filesystem. This contains the blast radius to a single feature. Deploy within the 365-day remediation window alongside the patch.client_max_body_size, limit_req_zone; Cloudflare rate-limiting rules; AWS ALB request-rate rules) to cap upload size and throttle requests per source IP on upload routes. This slows the orphan accumulation rate to the point where disk monitoring has ample time to alert. A 1 MB cap and 10 req/s/IP limit makes filling a 50 GB volume take weeks instead of hours.node_filesystem_avail_bytes, Datadog disk check, CloudWatch DiskSpaceUtilization) alerts at 80% and triggers an automated cleanup of orphaned files in the upload directory (files older than N minutes with no matching application record). This converts the DoS from 'silent until outage' to 'detected and remediated automatically'.diskStorage to memoryStorage (for small files) or a cloud SDK (S3, GCS, Azure Blob). memoryStorage is explicitly unaffected by this CVE since no filesystem writes occur. This eliminates the vulnerability class entirely for your application, though it may require code changes to downstream consumers of the uploaded file.- Helmet.js or CORS headers — these are HTTP response headers and have no effect on how multer handles incoming multipart request bodies or aborted connections.
- Node.js
--max-old-space-size— this limits heap memory, not disk writes. The orphaned files are written byfs.WriteStreamto the filesystem, not held in V8 memory. multer({ limits: { fileSize: N } })— thefileSizelimit only triggers after the file is being written. The race condition occurs in the abort-cleanup path *regardless* of size limits, so even a 1-byte orphan counts toward disk exhaustion at scale.- Upgrading to multer 2.2.0 or 2.3.0 — these are the *vulnerable* versions. The CVE-2026-5038 fix in 2.2.0 is what introduced the incomplete cleanup that this CVE bypasses. Only 2.4.0+ is safe.
The supporting signals.
| In-the-wild exploitation | None observed. Not listed in CISA KEV. No GreyNoise tags or Shadowserver campaigns reference this CVE. No reports of active exploitation from any ISAC or threat-intel feed as of 2026-10-23. |
|---|---|
| Proof-of-concept availability | No public PoC. pocindex.io returns no results. No GitHub repositories named after CVE-2026-88932. No ExploitDB, Nuclei template, or Metasploit module. The fix commit includes a regression test (test/orphan-before-path-assignment.js) that demonstrates the race, which could be adapted as a PoC. Exploitation is conceptually trivial (abort a curl --form mid-stream) but reliably winning the race requires async callbacks. |
| EPSS score | 0.00532 (0.532%) — 44th percentile. Indicates a below-average probability of exploitation in the next 30 days. Consistent with a DoS-only, no-PoC, library-level vulnerability. |
| KEV status | Not listed. No CISA Known Exploited Vulnerabilities entry. No binding operational directive applies. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L — 5.3 MEDIUM. Network-accessible, no auth required, but impact is availability-only and rated Low. The A:L reflects that disk fill is gradual and recoverable rather than an instant crash. |
| Affected versions | multer ≥ 2.2.0, < 2.4.0 (npm). Specifically versions 2.2.0, 2.2.1, 2.3.0. Only the diskStorage engine is affected; memoryStorage is immune. This is a bypass of the CVE-2026-5038 fix that shipped in 2.2.0. |
| Fixed versions | multer 2.4.0 (npm, published ~September 2026). Commit expressjs/multer@53337f9. No distro backports apply — multer is an npm package consumed directly. |
| Installed base / exposure | multer has ~8–20 million weekly npm downloads and 6,738 dependent packages. However, the vulnerable window (2.2.0–2.3.x) is narrow — many installations still run 1.x (pre-affected) or have already upgraded to 2.4.0. Shodan/Censys cannot fingerprint multer versions remotely; exposure estimates rely on npm download data. |
| Disclosure timeline | Disclosed 2026-09-14. GHSA GHSA-3pph-fpjx-jg34 published same day. NVD entry modified 2026-10-21. Predecessor CVE-2026-5038 was fixed in multer 2.2.0 (June 2026); this CVE is its incomplete-fix bypass. |
| Reporter / credit | Reported by euriconicacio. Fix developed by UlisesGascon and bjohansebas (Express.js / OpenJS Foundation maintainers). |
Sources.
Why this verdict
- DoS-only impact ceiling: The vulnerability provides no path to code execution, data exfiltration, or privilege escalation. The worst outcome is disk exhaustion on a single host, which is recoverable by clearing orphaned files and restarting the process. This hard-caps severity at MEDIUM regardless of other factors.
- Narrow version window: Only multer 2.2.0–2.3.x is affected — two minor releases spanning roughly three months. The 1.x branch (still widely deployed) and 2.4.0+ are immune. This significantly limits the reachable population.
- Race condition reliability: Exploiting the timing window requires the abort to land *after* the file write starts but *before* the storage engine assigns a path. Synchronous
destination/filenamecallbacks shrink this window to microseconds. Only async callbacks (DB lookups, hashing) make it reliably exploitable, and this is not the default configuration. - Compensating controls are effective and common: Disk quotas, separate upload volumes, container ephemeral storage limits, WAF rate-limiting on aborted connections, and basic disk monitoring all independently break the chain. Most production deployments have at least one of these.
- Role multiplier: multer is application-layer middleware running in Node.js user-space. It is not a high-value-role component (not an identity provider, hypervisor, kernel agent, network edge, or CI/CD system). In its typical deployment role (Express.js web app handling file uploads), the blast radius of disk exhaustion is *single-host, single-application* — not domain, fleet, or supply-chain scale. Even on a high-value application (e.g., a document-ingestion API for a regulated system), the outcome remains temporary DoS, not compromise. No high-value-role floor applies.
- No exploitation pressure: Zero in-the-wild exploitation, no public PoC, not KEV-listed, EPSS at 44th percentile. There is no urgency signal overriding the baseline assessment.
Why not higher?
Upgrading to HIGH would require either a path to code execution / data compromise, or a blast radius that reaches fleet or domain scale. This vulnerability offers neither — it is strictly availability-only, single-host DoS via gradual disk fill. The race condition is unreliable in default configurations, and standard infrastructure controls (disk quotas, monitoring, container limits) break the chain independently. There is no exploitation in the wild and no PoC to accelerate weaponisation.
Why not lower?
Dropping to LOW would understate the risk for the subset of deployments that expose unauthenticated upload endpoints with diskStorage, async callbacks, no disk quotas, and no monitoring — in that configuration, a sustained attack *will* fill the disk and cause an outage. The unauthenticated, network-accessible attack vector with no user interaction means any reachable instance is targetable. The 5.3 MEDIUM correctly balances the easy entry point against the limited impact.
Crowdsourced verification payload.
Run this on each application host (or in your CI pipeline against package-lock.json / yarn.lock). No special privileges needed — it reads node_modules or the lockfile. Example: bash check_cve_2026_88932.sh /opt/myapp
#!/usr/bin/env bash
# check_cve_2026_88932.sh — Detect multer versions vulnerable to CVE-2026-88932
# Usage: bash check_cve_2026_88932.sh <project_root>
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
set -euo pipefail
PROJECT_ROOT="${1:-.}"
VULN_MIN="2.2.0"
VULN_MAX="2.3.999" # anything < 2.4.0
FIXED="2.4.0"
version_gte() {
# returns 0 if $1 >= $2 using sort -V
[ "$(printf '%s\n%s' "$1" "$2" | sort -V | head -n1)" = "$2" ]
}
# Try to find installed multer version
MULTER_VERSION=""
# Method 1: node_modules package.json
if [ -f "${PROJECT_ROOT}/node_modules/multer/package.json" ]; then
MULTER_VERSION=$(grep '"version"' "${PROJECT_ROOT}/node_modules/multer/package.json" \
| head -1 | sed 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
fi
# Method 2: npm ls (fallback)
if [ -z "$MULTER_VERSION" ] && command -v npm &>/dev/null; then
MULTER_VERSION=$(cd "$PROJECT_ROOT" && npm ls multer --json 2>/dev/null \
| grep '"version"' | head -1 \
| sed 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)
fi
if [ -z "$MULTER_VERSION" ]; then
echo "UNKNOWN — multer not found in ${PROJECT_ROOT}"
echo "If multer is not a dependency, this CVE does not apply."
exit 2
fi
echo "Detected multer version: ${MULTER_VERSION}"
if version_gte "$MULTER_VERSION" "$FIXED"; then
echo "PATCHED — multer ${MULTER_VERSION} >= ${FIXED}, CVE-2026-88932 is fixed."
exit 0
elif version_gte "$MULTER_VERSION" "$VULN_MIN"; then
echo "VULNERABLE — multer ${MULTER_VERSION} is in the affected range (${VULN_MIN}–<${FIXED})."
echo "Upgrade to multer >= ${FIXED}: npm install multer@${FIXED}"
exit 1
else
echo "PATCHED — multer ${MULTER_VERSION} < ${VULN_MIN}, not affected by CVE-2026-88932."
echo "(Note: versions < 2.2.0 may be affected by the predecessor CVE-2026-5038.)"
exit 0
fi