Handlebars.js is a templating library downloaded roughly 172 million times a month. Two vulnerabilities have been disclosed in it with public technical detail and proof-of-concept code: CVE-2026-106446, scored 9.8, and CVE-2026-106445, scored 9.2. Both affect 4.0.0 up to 4.7.10, and both are fixed in 4.7.10.
Both end in code execution on the server, and each is a precise, instructive mistake.
A validator that only knew what it was looking for
Handlebars does not interpret templates. It compiles them — turning a template into JavaScript source that is then run. That is why it is fast, and it is the fact everything else here depends on.
Its compile and precompile functions accept not only a template string but a pre-parsed syntax tree, so that an application can parse once and compile later. When they receive a tree, they validate it — but only selected node types: path expressions, number literals and boolean literals.
Everything else passes through and is written into the generated JavaScript as-is.
So an attacker who can hand the library an object where it expected a string can put JavaScript expressions in the fields nobody checks. The advisory lists them: the length on a program's block parameters, a parameter depth that is not a path expression, a string literal whose value is not a string, a path expression whose original is not a string. None of those is validated, all of them are emitted, and what is emitted runs — in the server process when the compiled template renders, or wherever precompiled output is later loaded.
The validation was not wrong about the things it checked. It simply assumed the input was well-formed, which is a reasonable assumption for a parser's own output and the wrong one for anything an attacker can supply.
A deny list that runs one step late
The second flaw is smaller and sharper.
Handlebars keeps a deny list to stop templates from reaching through an object's prototype into the JavaScript runtime — the standard defence for this class of library, because getting hold of the Function constructor means getting arbitrary code.
Its property lookup returns the value before applying that deny list in one case: the constructor property, because constructor is an own property of Function.prototype rather than something inherited. The deny list never sees it.
Given a template the attacker controls, a configuration that allows prototype methods, and any function reachable in the template's data, a template can walk from that function to its prototype, from there to Function.prototype, and take the constructor out through the gap.
Two mistakes, one shape: a check that is correct about what it inspects and silent about everything it does not.
The third one this week
This is now the third template engine we have written about in a week.
GitLab's AI Gateway shipped a 9.9 where a prompt template escaped its sandbox and ran commands on the host. That was Jinja2 and a sandbox. This is Handlebars and a deny list. The engines differ; the position does not.
A template language is a programming language that someone has decided to make safe by subtraction — start with something expressive, then forbid the dangerous parts. Every such defence is a list of what is not allowed, and every list is a statement that the author thought of everything. The Handlebars deny list missed one own property. The Jinja2 sandbox missed a path back to the runtime. These are the same bug written twice.
Why 172 million downloads matters more than the score
A 9.8 in an application is a problem for that application. A 9.8 in a library at this volume is a problem for software nobody is thinking about.
Handlebars is a dependency of dependencies. It arrives inside frameworks, build tools, documentation generators, email systems and content pipelines, pulled in by something a developer chose for an unrelated reason. The number of teams running it deliberately is small. The number running it is enormous.
And the exploitable conditions are narrower than the score suggests — the first flaw needs an attacker-supplied object reaching compile, and the second needs a non-default configuration plus a controlled template. Whether a given application is reachable depends on how it feeds the library. Most will not be. The ones that are will not know, because nobody audits the call sites of a transitive dependency.
With proof-of-concept code public, the time to find out is now rather than after someone else does.
What to do
- Upgrade to 4.7.10. It is a patch release of a library you are probably already carrying.
- Find out whether you carry it at all, and where from. The dependency tree, not the package file, is the thing to search.
- Check your call sites. Anywhere compile or precompile is handed something derived from a request, rather than a template string your code wrote, is the exposure.
- If you set the option that allows prototype methods, turn it off unless you know why it is on. That is the precondition for the second flaw.
What is not established
- Whether either flaw has been exploited. Nothing published says so.
- How many downstream packages pass attacker-influenced input into compile, which is what decides real-world exposure.
- Who reported the flaws, which the advisories as reported do not make clear.
- Whether other paths into the compiler share the same missing validation.