Oct 24, 2026·10 min read·4 visits
An incomplete fix for CVE-2026-33943 in happy-dom versions 17.0.0 through 21.0.3 allows arbitrary code execution on the host Node.js server when JavaScript evaluation is enabled. By crafting an ECMAScript module import with an unsanitized alias containing a destructuring default value and a JSON import attribute, an attacker can execute arbitrary Node.js code during module compilation and instantiation.
CVE-2026-115895 is a high-severity code injection vulnerability (CWE-94) in the happy-dom npm package prior to version 21.0.4. The vulnerability exists within the ECMAScriptModuleCompiler component, which transforms ECMAScript module syntax into executable Node.js functions when JavaScript evaluation is enabled. Due to an incomplete fix for CVE-2026-33943, the compiler failed to validate local named import aliases against legal JavaScript identifier rules. Attackers supplying crafted module imports—specifically utilizing destructuring default initializers alongside JSON module attributes—can inject arbitrary JavaScript expressions directly into the host Node.js runtime context.
happy-dom is a Node.js implementation of a web browser designed for server-side rendering, testing, and web scraping without the overhead of a full headless browser binary. To support client-side scripts and modern web standards, happy-dom includes an ECMAScript Module (ESM) compilation subsystem located in packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts. When the enableJavaScriptEvaluation configuration setting is active, this compiler parses <script type="module"> blocks and external module imports, rewriting their import and export declarations into executable JavaScript statements run within the Node.js environment.
CVE-2026-115895 (tracked as GitHub Security Advisory GHSA-834v-8h66-3h79) is a Code Injection vulnerability classified under CWE-94 (Improper Control of Generation of Code). It affects happy-dom versions starting from 17.0.0 up to 21.0.3. The flaw stems from an incomplete remediation of an earlier vulnerability, CVE-2026-33943. While the patch for CVE-2026-33943 introduced validation for imported and exported identifiers in several code generation paths, it neglected to validate the local binding alias in named import clauses (import { name as alias } from '...').
Because the compiler interpolates the unvalidated alias string directly into a synthesized JavaScript object destructuring statement, an attacker who can control the source code of an evaluated ECMAScript module can alter the syntax of the generated code. By leveraging JavaScript destructuring default value assignments alongside JSON import attributes (with { type: 'json' }), the attacker can inject arbitrary JavaScript expressions that execute immediately within the host Node.js runtime process.
The root cause of CVE-2026-115895 lies in how ECMAScriptModuleCompiler parses and reconstructs named import statements using regular expressions rather than a full Abstract Syntax Tree (AST) parser. When the compiler encounters a named import block (matching {([^}]+)} in IMPORT_REGEXP), it splits the contents by commas and iterates over each specifier. Each specifier is further split on the as keyword (/\s+as\s+/) to separate the exported property name (nameParts[0]) from the local binding alias (nameParts[1]).
In versions prior to 21.0.4, the sanitization applied to both name and alias consisted solely of stripping single and double quotation marks via .replace(/["']/g, ''). The compiler then emitted a destructuring declaration to bind the properties from the internal $happy_dom.imports map:
let { "propertyName": targetBinding } = $happy_dom.imports.get('url');In standard ECMAScript syntax, object destructuring permits default initializers of the form let { "prop": localVar = defaultExpression } = obj;. If obj["prop"] evaluates to undefined, the JavaScript engine evaluates defaultExpression and assigns its result to localVar. Because ECMAScriptModuleCompiler did not restrict alias to valid identifier characters, supplying an import specifier such as missing as injected = <payload> caused the compiler to treat injected = <payload> as the alias. When the property missing was absent from the target module's exports, the injected <payload> expression executed immediately.
A secondary mechanism in the compiler normally prevents this injection from succeeding in standard ESM imports. To handle circular dependencies, the compiler collects imported properties into an array named resolvableCircularImports and later generates assignment expressions inside a resolver function (alias = $happy_dom.imports.get(...)). If alias contains injected = <payload>, the resulting circular resolver code becomes syntactically invalid due to chained assignments or invalid left-hand side expressions, throwing a SyntaxError before any code can run.
However, the circular import collection is gated by a check on the import attribute type (match[7]). Specifically, the compiler checks if (!match[7] || match[7] === 'esm') before pushing to resolvableCircularImports. By appending with { type: 'json' } to the import statement, match[7] evaluates to 'json'. This bypasses the circular import resolver generation altogether, leaving the injected destructuring assignment as the sole emission of alias and preserving valid JavaScript syntax throughout the compiled function.
The vulnerability and its corresponding fix are contained within packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts. Prior to commit fd163520b74d9fa146be91f7b4e4f46d6cccbe13, the named import processing loop performed minimal string manipulation before interpolating variables into the newCodeStart string buffer.
// Vulnerable code in packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts (prior to v21.0.4)
for (const part of parts) {
const nameParts = part.trim().split(/\s+as\s+/);
// FLAW: Only quotes are stripped; '=' and arbitrary expressions remain intact
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 });
}
// FLAW: Unvalidated alias and unescaped name are directly interpolated into generated code
newCodeStart += `let {${properties.map((property) => (property.alias ? `"${property.name}": ${property.alias}` : property.name)).join(', ')}} = $happy_dom.imports.get('${url}')${match[8]}`;note that if a specifier omits the as keyword (e.g., import { other = payload } from './dep.json' with { type: 'json' }), nameParts[1] is undefined, causing alias to fall back to nameParts[0]. In that scenario, name === alias evaluates to true, and property.name (containing other = payload) is emitted directly into let { other = payload } = ..., triggering the exact same destructuring default initializer injection without requiring the as keyword.
In version 21.0.4 (PR #2537, commit fd163520b74d9fa146be91f7b4e4f46d6cccbe13), the maintainer addressed the flaw by introducing strict regular expression validation for binding names and proper string escaping for property keys.
diff --git a/packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts b/packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts
index c6b9e28..a40bf5e 100644
--- a/packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts
+++ b/packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts
@@ -61,6 +61,11 @@ const IMPORT_REGEXP = /{([^}]+)}|\*\s+as\s+([a-zA-Z0-9-_$]+)|([a-zA-Z0-9-_$]+)/g
*/
const VALID_VARIABLE_NAME_REGEXP = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
+/**
+ * Valid local binding identifier regexp (supports Unicode identifiers).
+ */
+const VALID_BINDING_NAME_REGEXP = /^[\p{ID_Start}_$][\p{ID_Continue}$\u200c\u200d]*$/u;
+
/**
* Valid preceding token before a statement.
*/
@@ -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)) {
if (name === alias) {
properties.push({ name });
} else {
@@ -286,7 +291,7 @@ export default class ECMAScriptModuleCompiler {
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]}`;
+ newCodeStart += `let {${properties.map((property) => (property.alias ? `${JSON.stringify(property.name)}: ${property.alias}` : property.name)).join(', ')}} = $happy_dom.imports.get('${url}')${match[8]}`;
} else if (importMatch[2]) {
// Import all as
newCodeStart += `const ${importMatch[2]} = $happy_dom.imports.get('${url}')${match[8]}`;The patch introduces three defensive layers:
VALID_BINDING_NAME_REGEXP uses Unicode property escapes (\p{ID_Start} and \p{ID_Continue}) anchored with ^ and $ to verify that alias is strictly a single, valid ECMAScript identifier. Any alias containing whitespace, =, parentheses, or operators fails the test and is silently omitted from properties.name is expanded to strip backslashes (\), carriage returns (\r), newlines (\n), and Unicode line/paragraph separators (\u2028, \u2029), preventing line-continuation or quote-escaping attacks via the exported name."${property.name}" is replaced with ${JSON.stringify(property.name)}, ensuring that any remaining special characters in the property key are safely serialized as a valid string literal.Exploiting CVE-2026-115895 requires that the target Node.js application instantiates a happy-dom Window or Browser context with enableJavaScriptEvaluation enabled, and subsequently loads HTML markup or JavaScript module files controlled by an attacker. Note that if an attacker can already execute arbitrary inline <script> tags in an unsandboxed happy-dom instance, they may already have access to the execution environment depending on VM isolation settings; however, module compilation transforms code prior to execution and can be triggered via imported submodules or specific module-loading workflows.
To trigger the vulnerability, the attacker constructs an ECMAScript module containing a named import statement targeting any resolvable JSON endpoint (or mocked import entry) with the with { type: 'json' } attribute. Two primary syntax variants can be used:
import { missing as injected = <expression> } from './dep.json' with { type: 'json' };import { other = <expression> } from './dep.json' with { type: 'json' };The project's regression test suite (packages/happy-dom/test/module/ECMAScriptModuleCompiler.test.ts) demonstrates the exact proof-of-concept mechanism used to verify the flaw:
const code = `
import {
missing as injected = globalThis.__HAPPY_DOM_PWNED__ = 1337
} from './dep.json' with { type: 'json' };
import { other = globalThis.__HAPPY_DOM_PWNED__ = 1337 } from './dep.json' with { type: 'json' };
import { valid as ünicode$1 } from './dep.json' with { type: 'json' };
`;
const compiler = new ECMAScriptModuleCompiler(window);
const result = compiler.compile('http://localhost:8080/js/app/main.js', code);In a vulnerable version of happy-dom (< 21.0.4), calling compiler.compile() produces an execute function whose body contains:
let {"missing": injected = globalThis.__HAPPY_DOM_PWNED__ = 1337} = $happy_dom.imports.get('http://localhost:8080/js/app/dep.json');
let {other = globalThis.__HAPPY_DOM_PWNED__ = 1337} = $happy_dom.imports.get('http://localhost:8080/js/app/dep.json');When result.execute(...) is invoked by the module loader and 'http://localhost:8080/js/app/dep.json' resolves to an object that does not define missing or other, the Node.js engine evaluates the right-hand side of the default assignment. In a real-world attack scenario, replacing globalThis.__HAPPY_DOM_PWNED__ = 1337 with process-level calls allows full compromise of the underlying Node.js process.
CVE-2026-115895 is rated High severity with a CVSS v3.1 base score of 8.8 (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H). The Attack Vector is Network (AV:N) with Low Attack Complexity (AC:L) and No Privileges Required (PR:N). User Interaction is marked as Required (UI:R) because the host application or a user-triggered workflow must process external, untrusted HTML or JavaScript module content.
Successful exploitation results in arbitrary code execution within the Node.js process running happy-dom. Because ECMAScriptModuleCompiler compiles modules into functions executed within the host context, an attacker who achieves code execution can access Node.js runtime globals, read environment variables (such as cloud provider credentials or API keys), access the local filesystem, and spawn child processes.
This leads to a complete loss of Confidentiality (C:H), Integrity (I:H), and Availability (A:H) for the affected service. Applications at risk include server-side rendering (SSR) services, automated testing runners that evaluate third-party widgets or user-submitted components, and web scrapers or PDF/screenshot generators built on happy-dom with JavaScript evaluation turned on.
According to threat intelligence metrics, the EPSS score for CVE-2026-115895 is 0.00558 (45.78th percentile), indicating a low current probability of widespread automated exploitation in the wild. The vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog.
To remediate CVE-2026-115895, upgrade the happy-dom package to version 21.0.4 or later. Version 21.0.4 incorporates commit fd163520b74d9fa146be91f7b4e4f46d6cccbe13, which validates all local binding aliases against VALID_BINDING_NAME_REGEXP and properly serializes imported property names.
npm install happy-dom@^21.0.4> [!NOTE]
> If happy-dom is included as a transitive dependency via a test runner (such as Vitest) or another framework, verify the resolved version in package-lock.json, yarn.lock, or pnpm-lock.yaml using npm ls happy-dom, and apply overrides or resolutions if necessary.
For environments where an immediate package upgrade is not feasible, or as a defense-in-depth measure, disable JavaScript evaluation if the application only requires static DOM parsing, HTML manipulation, or server-side DOM traversal without client script execution. Ensure enableJavaScriptEvaluation is set to false (note that in recent major versions of happy-dom, script evaluation is disabled by default unless explicitly enabled in settings):
import { Window } from 'happy-dom';
const window = new Window({
settings: {
enableJavaScriptEvaluation: false,
suppressCodeGenerationFromStringsWarning: true
}
});From an architectural perspective, happy-dom is not designed as a secure sandbox for running untrusted third-party JavaScript code. If an application must evaluate untrusted HTML and JavaScript modules, happy-dom instances should be isolated inside separate OS-level containers, microVMs, or restricted worker processes with strict seccomp, filesystem, and network controls.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
happy-dom capricorn86 | >= 17.0.0, < 21.0.4 | 21.0.4 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-115895 (GHSA-834v-8h66-3h79) |
| Vulnerability Class | CWE-94: Improper Control of Generation of Code (Code Injection) |
| CVSS v3.1 Score | 8.8 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H) |
| Affected Component | happy-dom (ECMAScriptModuleCompiler.ts) |
| Affected Versions | >= 17.0.0, < 21.0.4 |
| Patched Version | 21.0.4 |
| EPSS Score | 0.56% (45.78th Percentile) |
| CISA KEV Status | Not Listed |
The software constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
An improper input validation and authorization bypass vulnerability exists in the LangGraph storage layer affecting langgraph, langgraph-checkpoint-postgres, and langgraph-checkpoint-sqlite. Because relational store implementations serialize namespace tuples into dot-delimited strings without validating labels on read, search, delete, and batch operations, an attacker who controls a namespace label can inject periods (.) to traverse namespace boundaries and access, modify, or delete data belonging to other namespaces.
CVE-2026-115886 (GHSA-2wqp-q3c6-ccj5) is a Server-Side Request Forgery (SSRF) and Unsafe Reflection vulnerability in MLflow versions 3.12.0rc0 prior to 3.17.0. Unauthenticated REST API endpoints accept serialized third-party scorer JSON payloads that are dynamically deserialized and reflectively instantiated without sanitizing constructor keyword arguments. When optional dependencies such as trulens and trulens-providers-litellm are installed, attackers can inject egress-steering arguments into the scorer payload, forcing the MLflow server to dispatch outbound HTTP requests to arbitrary internal destinations or cloud metadata services.
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.
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.