GitHub Security Advisory: GHSA-xhpv-hc6g-r9c6
Package: handlebars (NPM)
Severity: HIGH (CVSS 8.1)
CWE: CWE-94: Improper Control of Generation of Code ('Code Injection'), CWE-843: Access of Resource Using Incompatible Type ('Type Confusion')
Vulnerable versions: >= 4.0.0, <= 4.7.8
Patched in: 4.7.9

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

Details:
## Summary

A crafted object placed in the template context can bypass all conditional guards in `resolvePartial()` and cause `invokePartial()` to return `undefined`. The Handlebars runtime then treats the unresolved partial as a source that needs to be compiled, passing the crafted object to `env.compile()`. Because the object is a valid Handlebars AST containing injected code, the generated JavaScript executes arbitrary commands on the server. The attack requires the adversary to control a value that can be returned by a dynamic partial lookup.

## Description

The vulnerable code path spans two functions in `lib/handlebars/runtime.js`:

**`resolvePartial()`:** A crafted object with `call: true` satisfies the first branch condition (`partial.call`) and causes an early return of the original object itself, because none of the remaining conditionals (string check, `options.partials` lookup, etc.) match a plain object. The function returns the crafted object as-is.

**`invokePartial()`:** When `resolvePartial` returns a non-function object, `invokePartial` produces `undefined`. The runtime interprets `undefined` as "partial not yet compiled" and calls `env.compile(partial, ...)` where `partial` is the crafted AST object. The JavaScript code generator processes the AST and emits JavaScript containing the injected payload, which is then evaluated.

**Minimum prerequisites:**
1. The template uses a dynamic partial lookup: `{{> (lookup . "key")}}` or equivalent.
2. The adversary can set the value of the looked-up context property to a crafted object.

In server-side rendering scenarios where templates process user-supplied context data, this enables full Remote Code Execution.

## Proof of Concept

```javascript
const Handlebars = require('handlebars');

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);
// Prints: VULNERABLE: object -> dynamic partial -> RCE
```

## Workarounds

- **Use the runtime-only build** (`require('handlebars/runtime')`). Without `compile()`,  the fallback compilation path in `invokePartial` is unreachable.
- **Sanitize context data** before rendering: ensure no value in the context is a non-primitive  object that could be passed to a dynamic partial.
- **Avoid dynamic partial lookups** (`{{> (lookup ...)}}`) when context data is user-controlled.