high

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:

  1. happy-dom CVE-2026-115895 (GHSA-834v-8h66-3h79, CVSS 8.8 High, fixed in 21.0.4): An incomplete fix for CVE-2026-33943 in ECMAScriptModuleCompiler.ts validated export names in export { ... } but left imported local aliases in import { name as alias } from ... unvalidated before interpolating them into generated JavaScript destructuring bindings. Supplying import { 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')).
  2. undici CVE-2026-109869 (GHSA-xjj6-f33q-929c, CVSS 7.5 High), CVE-2026-109841 (GHSA-f72v-6w45-q472, Medium), and CVE-2026-109924 (GHSA-4qv8-2p9j-2685, Medium), fixed in 6.29.1, 7.30.1, and 8.12.2:
    • CVE-2026-109869: fetch() created this.body = new Readable({ read: resume }) before enforcing the 5-encoding limit on Content-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 wrapped await fetch() in try / catch.
    • CVE-2026-109841: In undici >= 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, causing fetch() to accept tampered response bodies without error or downgrade sha256-<good> SHA512-<bad> to the weaker digest.
    • CVE-2026-109924: EventSource failed to bound retained parser state across millions of tiny data: fields without a blank event separator or unfinished lines split across chunks.
  3. body-parser CVE-2026-111181 (GHSA-27r3-jxmh-p828, CVSS 7.5 High, fixed in 2.4.0 and 1.20.9) and multer CVE-2026-113872 (GHSA-q7qp-rjg7-q26m, CVSS 7.5 High, fixed in 2.5.0): Single-request unauthenticated Node.js process crashes caused by leaving a finished zlib stream piped to req without an error listener when trailing bytes follow a valid gzip/deflate frame (body-parser), or by a pending asynchronous fileFilter callback resuming a busboy stream after request teardown removed its error listeners (multer).

Affected packages and projects

EcosystemPackageCVE / GHSASeverityAffected versionsFixed versionsComponent
npmhappy-domCVE-2026-115895 (GHSA-834v-8h66-3h79)High (CVSS 8.8)>= 17.0.0, <= 21.0.321.0.4packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts
npmundiciCVE-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.26.29.1, 7.30.1, 8.12.2lib/web/fetch/index.js (httpNetworkFetch content-encoding limit)
npmundiciCVE-2026-109841 (GHSA-f72v-6w45-q472)Medium (CVSS 5.9)>= 7.15.0, < 7.30.1, >= 8.0.0, < 8.12.27.30.1, 8.12.2lib/web/subresource-integrity/subresource-integrity.js
npmundiciCVE-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.26.29.1, 7.30.1, 8.12.2lib/web/eventsource/* (EventSource stream parser)
npmbody-parserCVE-2026-111181 (GHSA-27r3-jxmh-p828)High (CVSS 7.5)< 1.20.9, >= 2.0.0, < 2.4.01.20.9, 2.4.0lib/read.js (contentstream decompression teardown)
npmmulterCVE-2026-113872 (GHSA-q7qp-rjg7-q26m)High (CVSS 7.5)>= 2.0.0, < 2.5.02.5.0lib/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' };
  1. Because replace(/["']/g, '') only strips ' and ", backtick template literals (`return process`) survive untouched.
  2. Because with { type: 'json' } sets match[7] to 'json', the if (!match[7] || match[7] === 'esm') guard skips pushing properties into resolvableCircularImports, which would otherwise emit a second assignment (injected = ...) that fails to parse.
  3. ECMAScriptModuleCompiler rewrites the import into valid JavaScript object destructuring with a default initializer:
    let { "missing": injected = globalThis.constructor.constructor(`return process`)()... } = $happy_dom.imports.get('...');
  4. When ./dep.json (or any JSON response) does not define a "missing" property, its value is undefined, so JavaScript evaluates the default initializer and reaches the host Node.js process object via globalThis.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-parser CVE-2026-111181 (commit f7075eae3a for 2.4.0 and 1260c3845f for 1.20.9): When inflate is enabled (the default) and a client sends Content-Encoding: gzip or deflate, contentstream() pipes req into zlib.createUnzip(). When the payload contains a complete valid compressed JSON frame followed by trailing garbage bytes, zlib emits 'end' as soon as the first frame finishes, raw-body removes its listeners and invokes body-parser’s callback, and the route responds HTTP 200. However, req was still piped into stream (zlib). When the trailing bytes arrived on req, the finished zlib stream 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 when verify was set and the Content-Type header carried an unsupported charset (such as charset=utf-999). The patch moves the iconv.encodingExists(encoding) check ahead of contentstream() and explicitly detaches the decompression stream once getBody completes:
    // 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()
    }
  • multer CVE-2026-113872 (commit 80a2d9df1d, 2.5.0): In lib/make-middleware.js, when a multipart upload contains two files and the application supplies an asynchronous fileFilter(req, file, cb), rejecting the second file calls done(err), which unpipes req and runs setImmediate(() => busboy.removeAllListeners()). When the first file’s still-pending async fileFilter callback later calls cb(null, true), it resumes fileStream, which unparks busboy on a truncated multipart body after all 'error' listeners have been removed from busboy. Commit 80a2d9df1d attaches a no-op busboy.on('error', function () {}) right after busboy.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

  1. Upgrade happy-dom, undici, body-parser, and multer:
    npm install happy-dom@^21.0.4 undici@^8.12.2 body-parser@^2.4.0 multer@^2.5.0
    (For projects on undici 6.x or 7.x, upgrade to undici@^6.29.1 or undici@^7.30.1; for Express 4 projects on body-parser 1.x, upgrade to body-parser@^1.20.9.)
  2. Workarounds if an immediate upgrade is blocked:
    • Happy DOM (CVE-2026-115895): Keep enableJavaScriptEvaluation disabled (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-...) in fetch() integrity options.
    • body-parser (CVE-2026-111181): Pass { inflate: false } to bodyParser.json() / bodyParser.urlencoded() if your API does not require compressed request bodies.
    • multer (CVE-2026-113872): Perform asynchronous validation inside the route handler after multer finishes rather than inside an async fileFilter callback.

Timeline

  • 2026-10-15: Undici publishes v6.29.1, v7.30.1, and v8.12.2 with commits 1325a87dfb (CVE-2026-109869), 18bc8a1e09 (CVE-2026-109841), and 789ef26461 (CVE-2026-109924).
  • 2026-10-20: Happy DOM merges PR #2537 (fd163520b7) and publishes happy-dom@21.0.4 to npm.
  • 2026-10-22: Express publishes body-parser@2.4.0 and 1.20.9 (f7075eae3a / 1260c3845f) and multer@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), and GHSA-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