CVE
CVE-2026-115895, CVE-2026-109869, CVE-2026-109841, CVE-2026-109924, CVE-2026-111181, CVE-2026-113872
CWE
CWE-94, CWE-345, CWE-400, CWE-755
Affected Surface
- npm package `happy-dom` versions `>= 17.0.0, <= 21.0.3` (fixed in `21.0.4`) when `enableJavaScriptEvaluation: true` is used to evaluate untrusted HTML or `<script type="module">` sources
- npm package `undici` versions `>= 6.23.0, < 6.29.1`, `>= 7.0.0, < 7.30.1`, and `>= 8.0.0, < 8.12.2` (fixed in `6.29.1`, `7.30.1`, and `8.12.2`)
- npm package `body-parser` versions `< 1.20.9` and `>= 2.0.0, < 2.4.0` (fixed in `1.20.9` and `2.4.0`) with default `inflate: true` options
- npm package `multer` versions `>= 2.0.0, < 2.5.0` (fixed in `2.5.0`) when configured with an asynchronous `fileFilter` callback
TL;DR
On 23 October 2026 (20:53 to 22:14 UTC), the GitHub Advisory Database published a batch of reviewed security advisories across four heavily downloaded Node.js packages:
happy-domCVE-2026-115895(GHSA-834v-8h66-3h79, CVSS 8.8 High, fixed in21.0.4): An incomplete fix forCVE-2026-33943inECMAScriptModuleCompiler.tsvalidated export names inexport { ... }but left imported local aliases inimport { name as alias } from ...unvalidated before interpolating them into generated JavaScript destructuring bindings. Supplyingimport { missing as injected = globalThis.constructor.constructor(`return process`)()... } from './dep.json' with { type: 'json' }turns invalid ESM syntax into a destructuring default initializer that executes in the host Node.js process (process.getBuiltinModule('node:fs')).undiciCVE-2026-109869(GHSA-xjj6-f33q-929c, CVSS 7.5 High),CVE-2026-109841(GHSA-f72v-6w45-q472, Medium), andCVE-2026-109924(GHSA-4qv8-2p9j-2685, Medium), fixed in6.29.1,7.30.1, and8.12.2:CVE-2026-109869:fetch()createdthis.body = new Readable({ read: resume })before enforcing the 5-encoding limit onContent-Encoding. When a remote server returned six content codings and closed the connection early, an unhandled'error'event on the orphaned internal stream terminated the Node.js process even when the caller wrappedawait fetch()intry / catch.CVE-2026-109841: Inundici >= 7.15.0,fetch()checked Subresource Integrity (integrity) algorithm tokens against a lowercase-only set (sha256,sha384,sha512) without ASCII-lowercasing the input token. Valid uppercase or mixed-case tokens (SHA256-...,Sha384-...) were treated as unknown algorithms and skipped, causingfetch()to accept tampered response bodies without error or downgradesha256-<good> SHA512-<bad>to the weaker digest.CVE-2026-109924:EventSourcefailed to bound retained parser state across millions of tinydata:fields without a blank event separator or unfinished lines split across chunks.
body-parserCVE-2026-111181(GHSA-27r3-jxmh-p828, CVSS 7.5 High, fixed in2.4.0and1.20.9) andmulterCVE-2026-113872(GHSA-q7qp-rjg7-q26m, CVSS 7.5 High, fixed in2.5.0): Single-request unauthenticated Node.js process crashes caused by leaving a finishedzlibstream piped toreqwithout an error listener when trailing bytes follow a validgzip/deflateframe (body-parser), or by a pending asynchronousfileFiltercallback resuming abusboystream after request teardown removed its error listeners (multer).
Affected packages and projects
| Ecosystem | Package | CVE / GHSA | Severity | Affected versions | Fixed versions | Component |
|---|---|---|---|---|---|---|
| npm | happy-dom | CVE-2026-115895 (GHSA-834v-8h66-3h79) | High (CVSS 8.8) | >= 17.0.0, <= 21.0.3 | 21.0.4 | packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts |
| npm | undici | CVE-2026-109869 (GHSA-xjj6-f33q-929c) | High (CVSS 7.5) | >= 6.23.0, < 6.29.1, >= 7.18.0, < 7.30.1, >= 8.0.0, < 8.12.2 | 6.29.1, 7.30.1, 8.12.2 | lib/web/fetch/index.js (httpNetworkFetch content-encoding limit) |
| npm | undici | CVE-2026-109841 (GHSA-f72v-6w45-q472) | Medium (CVSS 5.9) | >= 7.15.0, < 7.30.1, >= 8.0.0, < 8.12.2 | 7.30.1, 8.12.2 | lib/web/subresource-integrity/subresource-integrity.js |
| npm | undici | CVE-2026-109924 (GHSA-4qv8-2p9j-2685) | Medium (CVSS 5.9) | >= 6.5.0, < 6.29.1, >= 7.0.0, < 7.30.1, >= 8.0.0, < 8.12.2 | 6.29.1, 7.30.1, 8.12.2 | lib/web/eventsource/* (EventSource stream parser) |
| npm | body-parser | CVE-2026-111181 (GHSA-27r3-jxmh-p828) | High (CVSS 7.5) | < 1.20.9, >= 2.0.0, < 2.4.0 | 1.20.9, 2.4.0 | lib/read.js (contentstream decompression teardown) |
| npm | multer | CVE-2026-113872 (GHSA-q7qp-rjg7-q26m) | High (CVSS 7.5) | >= 2.0.0, < 2.5.0 | 2.5.0 | lib/make-middleware.js (busboy teardown with async fileFilter) |
Technical analysis
1. CVE-2026-115895: Happy DOM ESM import { ... as alias } code injection (21.0.4)
Happy DOM v17.0.0 introduced ECMAScriptModuleCompiler to rewrite ES module import and export statements into executable JavaScript functions when enableJavaScriptEvaluation: true is configured. Earlier in 2026, CVE-2026-33943 (GHSA-6q6h-j7hj-3r64) patched expression injection in export { ... } declarations by testing importName against VALID_VARIABLE_NAME_REGEXP.
However, the named import { ... } branch in packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts only stripped single and double quotes from alias and never checked whether alias was a valid identifier:
// packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts in happy-dom <= 21.0.3
for (const part of parts) {
const nameParts = part.trim().split(/\s+as\s+/);
const alias = (nameParts[1] || nameParts[0]).replace(/["']/g, '');
const name = nameParts[0].replace(/["']/g, '');
if (alias && name) {
if (name === alias) {
properties.push({ name });
} else {
properties.push({ name, alias });
}
}
}
if (!match[7] || match[7] === 'esm') {
resolvableCircularImports.push({ url, properties });
}
newCodeStart += `let {${properties
.map((property) => (property.alias ? `"${property.name}": ${property.alias}` : property.name))
.join(', ')}} = $happy_dom.imports.get('${url}')${match[8]}`;
Look at what happens when a <script type="module"> tag contains this syntactically invalid ESM import with a JSON import attribute (with { type: 'json' }):
import {
missing as injected = globalThis.constructor.constructor(`return process`)()
.getBuiltinModule(`node:fs`)
.mkdirSync(`/tmp/HAPPY_DOM_RCE_PROOF`)
} from './dep.json' with { type: 'json' };
- Because
replace(/["']/g, '')only strips'and", backtick template literals (`return process`) survive untouched. - Because
with { type: 'json' }setsmatch[7]to'json', theif (!match[7] || match[7] === 'esm')guard skips pushingpropertiesintoresolvableCircularImports, which would otherwise emit a second assignment (injected = ...) that fails to parse. ECMAScriptModuleCompilerrewrites the import into valid JavaScript object destructuring with a default initializer:let { "missing": injected = globalThis.constructor.constructor(`return process`)()... } = $happy_dom.imports.get('...');- When
./dep.json(or any JSON response) does not define a"missing"property, its value isundefined, so JavaScript evaluates the default initializer and reaches the host Node.jsprocessobject viaglobalThis.constructor.constructor('return process')().
Commit fd163520b7 (PR #2537, released in happy-dom@21.0.4) validates alias against VALID_BINDING_NAME_REGEXP (/^[\\p{ID_Start}_$][\\p{ID_Continue}$\\u200c\\u200d]*$/u) and serializes property.name with JSON.stringify():
--- a/packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts
+++ b/packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts
@@ -274,8 +279,8 @@ export default class ECMAScriptModuleCompiler {
for (const part of parts) {
const nameParts = part.trim().split(/\s+as\s+/);
const alias = (nameParts[1] || nameParts[0]).replace(/["']/g, '');
- const name = nameParts[0].replace(/["']/g, '');
- if (alias && name) {
+ const name = nameParts[0].replace(/["'\\\r\n\u2028\u2029]/g, '');
+ if (alias && name && VALID_BINDING_NAME_REGEXP.test(alias)) {
2. CVE-2026-109869 and CVE-2026-109841: Undici fetch() unhandled stream crash and SRI case bypass
In undici (lib/web/fetch/index.js), httpNetworkFetch() allocated this.body = new Readable({ read: resume }) before checking whether the response’s Content-Encoding header exceeded maxContentEncodings = 5. When a malicious server returned six content codings (such as Content-Encoding: gzip, gzip, gzip, gzip, gzip, gzip), Content-Length: 1, and immediately closed the TCP socket:
--- a/lib/web/fetch/index.js
+++ b/lib/web/fetch/index.js
@@ -2122,8 +2122,6 @@ async function httpNetworkFetch (
}
location = headersList.get('location', true)
- this.body = new Readable({ read: resume })
-
const decoders = []
...
const maxContentEncodings = 5
if (codings.length > maxContentEncodings) {
- reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
+ this.abort(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
return true
}
In the unpatched code, reject(...) rejected the outer fetch() promise and returned early without calling this.abort() or attaching the onError listener to this.body. When the socket closed with 0 body bytes despite Content-Length: 1, the underlying client invoked this.body.destroy(new SocketError('terminated')) on an orphaned Readable stream with no 'error' listener, crashing the Node.js process even when the caller had try { await fetch(url) } catch {}. Commit 1325a87dfb moves this.body = new Readable(...) below the codings.length loop and calls this.abort(...).
Separately, in CVE-2026-109841 (commit 18bc8a1e09), lib/web/subresource-integrity/subresource-integrity.js validated SRI hash algorithms via validSRIHashAlgorithmTokenSet.has(algorithm) without normalizing algorithm to ASCII lowercase first. Per the W3C Subresource Integrity specification, algorithm tokens are case-insensitive (SHA256-... and Sha384-... are valid), while unknown algorithms must be ignored for forward compatibility. Because validSRIHashAlgorithmTokenSet only contained 'sha256', 'sha384', and 'sha512', any caller passing fetch(url, { integrity: 'SHA256-...' }) had its integrity list reduced to [], causing fetch() to accept arbitrary tampered responses without throwing an integrity mismatch error.
3. CVE-2026-111181 (body-parser) and CVE-2026-113872 (multer): single-request Express middleware crashes
Both Express middleware advisories involve stream teardown paths that leave an internal parser stream without an 'error' listener while additional bytes or callbacks are still in flight:
body-parserCVE-2026-111181(commitf7075eae3afor2.4.0and1260c3845ffor1.20.9): Wheninflateis enabled (the default) and a client sendsContent-Encoding: gzipordeflate,contentstream()pipesreqintozlib.createUnzip(). When the payload contains a complete valid compressed JSON frame followed by trailing garbage bytes,zlibemits'end'as soon as the first frame finishes,raw-bodyremoves its listeners and invokesbody-parser’s callback, and the route responds HTTP200. However,reqwas still piped intostream(zlib). When the trailing bytes arrived onreq, the finishedzlibstream emitted an unhandled'error'event (unexpected end of file/invalid block type) and crashed the Node.js process. A second vector hit the same unpiped-stream gap whenverifywas set and theContent-Typeheader carried an unsupportedcharset(such ascharset=utf-999). The patch moves theiconv.encodingExists(encoding)check ahead ofcontentstream()and explicitly detaches the decompression stream oncegetBodycompletes:// lib/read.js in body-parser 2.4.0 / 1.20.9 if (stream !== req) { req.unpipe(stream) stream.on('error', () => {}) stream.destroy() req.resume() }multerCVE-2026-113872(commit80a2d9df1d,2.5.0): Inlib/make-middleware.js, when a multipart upload contains two files and the application supplies an asynchronousfileFilter(req, file, cb), rejecting the second file callsdone(err), which unpipesreqand runssetImmediate(() => busboy.removeAllListeners()). When the first file’s still-pending asyncfileFiltercallback later callscb(null, true), it resumesfileStream, which unparksbusboyon a truncated multipart body after all'error'listeners have been removed frombusboy. Commit80a2d9df1dattaches a no-opbusboy.on('error', function () {})right afterbusboy.removeAllListeners().
Detection and remediation
1. Check installed versions and search for vulnerable call sites
Run the following commands across your repositories and lockfiles:
# Check installed versions in node_modules
npm ls happy-dom undici body-parser multer 2>/dev/null || true
# Search lockfiles for vulnerable version ranges
rg -n '"(happy-dom|undici|body-parser|multer)"\s*:\s*"' \
package.json package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null
# Search for Happy DOM JavaScript evaluation, uppercase SRI tokens, or async multer fileFilter hooks
rg -n 'enableJavaScriptEvaluation|integrity\s*:\s*["'\''`]SHA|fileFilter\s*:' \
--glob '*.{js,jsx,ts,tsx,mjs,cjs}' . 2>/dev/null
2. Upgrade to patched releases
- Upgrade
happy-dom,undici,body-parser, andmulter:
(For projects onnpm install happy-dom@^21.0.4 undici@^8.12.2 body-parser@^2.4.0 multer@^2.5.0undici6.xor7.x, upgrade toundici@^6.29.1orundici@^7.30.1; for Express 4 projects onbody-parser1.x, upgrade tobody-parser@^1.20.9.) - Workarounds if an immediate upgrade is blocked:
- Happy DOM (
CVE-2026-115895): KeepenableJavaScriptEvaluationdisabled (false) whenever parsing untrusted HTML, or run Happy DOM inside an isolated container/VM rather than the main application process. - Undici (
CVE-2026-109841): Always use lowercase algorithm prefixes (sha256-...,sha384-...,sha512-...) infetch()integrityoptions. body-parser(CVE-2026-111181): Pass{ inflate: false }tobodyParser.json()/bodyParser.urlencoded()if your API does not require compressed request bodies.multer(CVE-2026-113872): Perform asynchronous validation inside the route handler aftermulterfinishes rather than inside an asyncfileFiltercallback.
- Happy DOM (
Timeline
- 2026-10-15: Undici publishes
v6.29.1,v7.30.1, andv8.12.2with commits1325a87dfb(CVE-2026-109869),18bc8a1e09(CVE-2026-109841), and789ef26461(CVE-2026-109924). - 2026-10-20: Happy DOM merges PR
#2537(fd163520b7) and publisheshappy-dom@21.0.4to npm. - 2026-10-22: Express publishes
body-parser@2.4.0and1.20.9(f7075eae3a/1260c3845f) andmulter@2.5.0(80a2d9df1d). - 2026-10-23 (20:53 to 22:14 UTC): The GitHub Advisory Database and NVD publish the reviewed advisories
GHSA-27r3-jxmh-p828(CVE-2026-111181),GHSA-834v-8h66-3h79(CVE-2026-115895),GHSA-q7qp-rjg7-q26m(CVE-2026-113872),GHSA-f72v-6w45-q472(CVE-2026-109841),GHSA-4qv8-2p9j-2685(CVE-2026-109924), andGHSA-xjj6-f33q-929c(CVE-2026-109869).
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.
References
- Happy DOM advisory GHSA-834v-8h66-3h79 (CVE-2026-115895)
- capricorn86/happy-dom commit fd163520b7 (PR #2537: validate ESM import binding aliases)
- Undici advisory GHSA-xjj6-f33q-929c (CVE-2026-109869: excessive content-encodings crash)
- Undici advisory GHSA-f72v-6w45-q472 (CVE-2026-109841: non-lowercase SRI token bypass)
- Undici advisory GHSA-4qv8-2p9j-2685 (CVE-2026-109924: EventSource parser memory exhaustion)
- body-parser advisory GHSA-27r3-jxmh-p828 (CVE-2026-111181: trailing bytes after compressed body)
- multer advisory GHSA-q7qp-rjg7-q26m (CVE-2026-113872: pending async fileFilter after teardown)
- Safeguard: Nine undici advisories ahead of Node.js's 21 October security release