If you use Elo’s Ruby target, upgrade to 1.0.1 now. The other targets are affected by less-severe issues but we recommend upgrading everywhere.

Over the past week, Atsushi Sada conducted a thorough audit of the Elo compiler and reported a series of injection vulnerabilities. Fixing the first led us to a second, which led to a third. 1.0.1 bundles all five fixes.

Risk assessment

Here’s what was broken, how bad it is, and who’s affected.

1. Ruby interpolation injection — critical, Ruby target

Ruby double-quoted strings interpolate #{...}, #@ivar, #@@cvar, and #$gvar. Elo string literals and subtype-constraint labels were emitted into double-quoted Ruby strings without escaping #. Any attacker-controlled string containing #{...} became executable code at Ruby runtime.

This Elo source:

'#{`rm -rf ~`}'

…was compiled to this Ruby:

"#{`rm -rf ~`}"   # shell exec at runtime

Exploitable by: anyone who can submit an Elo source string that your application compiles to Ruby and runs. No special access to the AST API required. Plain Elo source is enough.

Severity: remote code execution.

2. JavaScript sandbox escape via meta-properties — critical, JS target

Elo’s member-access rule restricts .property to data types, but constructor, __proto__, and prototype slipped through. An Elo expression like ({}).constructor.constructor('return process')() reaches the Function constructor via the prototype chain and executes arbitrary JavaScript.

The playground and eloAdapter.parse() evaluate compiled JS via new Function(...), so a malicious expression becomes full RCE in that process.

Exploitable by: anyone who can submit an Elo expression that your application evaluates via the JS target.

Severity: remote code execution.

3. Constraint/guard label injection — medium, JS and Ruby

Label escaping in the JS emitter and Ruby’s single-quoted branch escaped only ', not \. A label ending in \ silently ate the closing quote and allowed subsequent bytes to be swallowed into the string literal — a SyntaxError today, injectable with a more artful payload.

Exploitable by: anyone who controls the text of a subtype constraint or guard label. Less common than string literals, but possible if labels are built from user input.

Severity: denial of service today, code injection with effort.

4. AST-API injection — medium, all targets

The Elo parser rejects shell-breakout payloads in identifiers. But the compiler API (compileToJavaScript, compileToRuby, compileToPython, compileToSQL) accepts ASTs directly. A caller who builds an AST with attacker-controlled strings in identifier slots (variable names, lambda parameters, member-access properties, object-literal keys, datapath segments, let-binding names) could inject arbitrary code in any target.

Exploitable by: anyone whose application constructs ASTs from untrusted JSON or similar. Atypical, but real for library consumers.

Severity: code injection, target-dependent.

5. fetch() denylist bypass — low, defense-in-depth, all targets

The member-access denylist (fix #2) lived in the IR transform, but fetch() reaches a runtime helper that does dynamic obj[segment] lookup. So fetch({}, .constructor) and fetch({}, 'constructor') bypassed the check.

Today this is blocked from full RCE only by a typeof !== 'object' guard inside the fetch helper — structural luck, not principled defense. A future refactor could reopen it.

Severity: defense-in-depth fix. No known live exploit.

What to do

Upgrade:

npm install @enspirit/elo@1.0.1

If you compile to Ruby with any user-controllable input, do this now. For other targets, the risk profile depends on whether your application evaluates compiled code from untrusted Elo source — if it does (the playground, expression editors, dashboards that accept user formulas), upgrade promptly.

No breaking changes. No API surface changed. The fixes are internal to the compiler. Expressions and ASTs that were valid in 1.0.0 remain valid in 1.0.1 — except the ones that were exploiting these bugs, which now fail at compile or transform time with a clear error.

Credits

Thanks to Atsushi Sada for the report, the follow-ups, and the patience to keep digging after each fix. Three of the five vulnerabilities were reported directly; the other two were uncovered while auditing the same patterns elsewhere in the codebase.

Safety is one of Elo’s four design principles. Receiving a careful security audit is a gift — it’s how the principle becomes real rather than aspirational. Thank you.

Verify the fix

Every vulnerability has regression tests — unit tests in test/unit/security.unit.test.ts and acceptance fixtures under test/fixtures/security/ that run the compiled output in every target and fail if the payload executes. The full test suite — unit, integration, and acceptance across four runtimes — passes on every commit.

Try Elo, read the full Changelog, or open an issue.