
Handlebars.js AST Injection Remote Code Execution Vulnerability
This post is a research article published by EQSTLab.
Referenced PoC: dinhvaren/cve-2026-33937
★ CVE-2026-33937 Handlebars.js AST Injection Remote Code Execution PoC ★
https://github.com/user-attachments/assets/99e383e7-e71f-4e10-8c62-f50abff8b4f5
CVE-2026-33937 : Handlebars.js AST Injection Remote Code Execution Vulnerability
| Affected Versions | 4.0.0 – 4.7.8 |
| Patched Version | 4.7.9 |
| CVSS Score | 9.8 (Critical) |
A Type Confusion vulnerability in Handlebars.js arises from the fact that Handlebars.compile() accepts not only a template string but also a pre-parsed AST object (JSON) as its input. An attacker who controls the value passed to compile() can supply a crafted AST object in which the value field of a NumberLiteral node is replaced with an arbitrary JavaScript string. Because the internal code generator inserts that field directly into the emitted JavaScript function body without sanitization, the resulting function executes attacker-controlled code when rendered — achieving Remote Code Execution on the server.
Build and run the vulnerable environment using Docker:
Build Image
docker build -t cve-2026-33937-server .
Run Container
docker run --name cve-2026-33937 -p 12701:12701 -p 9229:9229 cve-2026-33937-server
Or use the provided npm scripts:
npm run docker:up # build + run in one step
The application is served at http://localhost:12701.
Web UI
Open http://localhost:12701 in a browser. The page presents a B2B email builder interface. User-supplied field values are interpolated into a Handlebars template string on the client side and then posted to the server for rendering. Inject a malicious Handlebars payload via the input fields or directly via the API.
Direct API
# POST crafted template to the vulnerable endpoint
curl -s -X POST http://localhost:12701/api/email/preview \
-H "Content-Type: application/json" \
-d '{"subject":"test","editorTemplateData":"<PAYLOAD>"}'
Replace <PAYLOAD> with an AST injection payload targeting Handlebars.compile().
POST /api/email/preview
The server accepts a JSON body containing editorTemplateData and passes it directly to Handlebars.compile() with no sanitization or allowlist enforcement:
// app.js
const renderEmail = Handlebars.compile(editorTemplateData);
Handlebars.compile() is documented as accepting a template string, but its internal type check also allows a pre-parsed AST object to be passed directly. Inside javascript-compiler.js, the NumberLiteral() visitor emits the node's value field verbatim into the generated JavaScript source without any type validation or sanitization:
// javascript-compiler.js (simplified)
NumberLiteral(number) {
this.pushStackLiteral(number.value); // value inserted as-is into emitted JS
}
If an attacker supplies a crafted AST object where number.value is a string containing arbitrary JavaScript (e.g., "1; require('child_process').execSync(...)") instead of a numeric literal, the emitted function body contains and executes that code at render time.
The dangerous pattern in the vulnerable application is:
const render = Handlebars.compile(userInput); // userInput may be a crafted AST object
render(safeContextData); // attacker code runs here
Note on exploitation technique: Existing public PoCs (including the referenced one) achieve the injection via a
NumberLiteralnode combined with alookuphelper. This PoC confirms the same RCE primitive using aBooleanLiteralnode combined with thelogbuilt-in helper, demonstrating that the type confusion is not limited to a single node type or helper function.
/api/email/preview with a crafted editorTemplateData value.Handlebars.compile() on the attacker-controlled AST object.render() triggers execution of arbitrary JavaScript in the server process.When Handlebars is implemented in a Node.js backend environment, this vulnerability leads to a server-side code execution path, unlike standard client-side XSS. The attack does not require any output to be reflected to a user; the payload executes within the Node.js process with the same privileges as the application. Depending on the deployment:
/etc/passwd) can be read and exfiltrated.+-------------------------------------------+
| Attacker |
+-------------------------------------------+
|
| POST /api/email/preview
| {"editorTemplateData": "<malicious payload>"}
v
+-------------------------------------------+
| Handlebars.compile(editorTemplateData) |
| (No sanitization — app.js:17) |
+-------------------------------------------+
|
| AST node injection
| breaks template sandbox
v
+-------------------------------------------+
| Arbitrary JS Execution (Server Process) |
+-------------------------------------------+
|
| Read sensitive files, spawn shell,
| exfiltrate secrets, etc.
v
+-------------------------------------------+
| Remote Code Execution (RCE) |
+-------------------------------------------+
Upgrade Handlebars.js to 4.7.9 or later.
Version 4.7.9 introduces strict input type validation in compile(), rejecting non-string arguments before code generation begins.
Enforce a type check before calling compile().
If an immediate upgrade is not possible, validate that the argument is always a string at the call site:
if (typeof templateInput !== 'string') throw new TypeError('Template must be a string');
const render = Handlebars.compile(templateInput);
Use handlebars/runtime for build-time pre-compilation.
Pre-compile templates at build time with the Handlebars CLI and ship only the runtime bundle. The runtime build does not include compile(), eliminating the attack surface entirely for production deployments.
This repository is intended solely for security research, education, and controlled vulnerability demonstration. It must not be used to test or exploit systems without explicit written authorization from the system owner. The purpose of this project is to help security researchers, defenders, and developers understand the vulnerability, validate exposure in controlled lab environments, and apply effective mitigations.