Back to Blog
critical SEVERITY9 min read

Critical RCE in Handlebars.js: How CVE-2026-33937 Was Fixed

A critical Remote Code Execution vulnerability (CVE-2026-33937) was discovered in Handlebars.js that allows attackers to execute arbitrary code by crafting malicious Abstract Syntax Tree objects passed to the `compile()` function. This post breaks down how the vulnerability works, why it's dangerous, and how upgrading to Handlebars 4.7.9 closes the attack vector.

O
By orbisai0security
May 8, 2026
#security#rce#javascript#handlebars#cve#dependency-management#nodejs

Critical RCE in Handlebars.js: Understanding and Fixing CVE-2026-33937

TL;DR: A critical Remote Code Execution vulnerability in Handlebars.js (CVE-2026-33937) allows attackers to execute arbitrary code through crafted Abstract Syntax Tree (AST) objects. The fix is straightforward: upgrade to Handlebars 4.7.9.


Introduction

If your Node.js application uses Handlebars.js for templating — and millions do — you need to pay attention to CVE-2026-33937. This critical-severity vulnerability allows an attacker to achieve Remote Code Execution (RCE) by passing a specially crafted Abstract Syntax Tree (AST) object to Handlebars' compile() function.

RCE vulnerabilities are among the most severe class of security issues. Unlike XSS or CSRF, which typically require a victim to take an action in a browser, RCE means an attacker can run their own code directly on your server. That's game over for confidentiality, integrity, and availability.

Handlebars.js is downloaded tens of millions of times per week on npm. It's embedded in build tools, static site generators, email templating engines, and countless web applications. A vulnerability here has a massive blast radius.


The Vulnerability Explained

What Is Handlebars.js?

Handlebars.js is a popular JavaScript templating library that lets you build semantic templates. You write templates like:

<h1>Hello, {{name}}!</h1>
<p>You have {{messages.length}} new messages.</p>

Handlebars compiles these templates into JavaScript functions that can be called with a data context. This compilation step is at the heart of this vulnerability.

What Is an Abstract Syntax Tree (AST)?

When Handlebars parses a template string, it first converts it into an Abstract Syntax Tree (AST) — a structured, tree-like representation of the template's syntax. The compile() function can accept either a raw template string or a pre-parsed AST object.

This dual-input capability is where things get dangerous.

How the Vulnerability Works

The vulnerability exists in how Handlebars' compile() function processes AST objects. When an attacker can control or influence the AST object passed to compile(), they can craft a malicious AST that, when processed by the compiler, causes arbitrary JavaScript code to be generated and executed.

Here's a simplified conceptual illustration of the attack:

const Handlebars = require('handlebars');

// A normal, safe template compilation
const safeTemplate = Handlebars.compile("Hello, {{name}}!");
safeTemplate({ name: "World" }); // "Hello, World!"

// A crafted malicious AST object (conceptual — CVE-2026-33937)
const maliciousAST = {
  type: "Program",
  body: [{
    type: "MustacheStatement",
    // Attacker-controlled node properties that trick the compiler
    // into generating and executing arbitrary code...
    path: {
      type: "PathExpression",
      // Crafted payload that escapes the template sandbox
      original: "__proto__",
      // ... additional crafted properties
    }
  }]
};

// If an application passes untrusted AST objects to compile(),
// the attacker's code runs on the server
const dangerous = Handlebars.compile(maliciousAST);
dangerous({}); // 💥 Arbitrary code execution

The root cause is insufficient validation of AST node properties before they are used to generate JavaScript code. The compiler trusts the structure and content of the AST too liberally, allowing carefully constructed node objects to break out of the intended template execution context.

Attack Scenarios

Scenario 1: Applications Accepting User-Provided Templates

This is the most direct attack path. If your application allows users to submit templates that are then compiled server-side (e.g., a custom email template builder, a report generator, or a CMS with template editing), an attacker could submit a template that, when parsed and compiled, produces a malicious AST.

// ⚠️ DANGEROUS: Never compile untrusted user input server-side
app.post('/preview-template', (req, res) => {
  const userTemplate = req.body.template; // Attacker controls this
  const compiled = Handlebars.compile(userTemplate); // 💥 Potential RCE
  res.send(compiled({}));
});

Scenario 2: Deserialization of Stored AST Objects

Some applications optimize performance by pre-parsing templates into AST objects and storing them (in a database, cache, or file system). If an attacker can tamper with those stored AST objects — through a separate injection vulnerability, a compromised cache, or an insecure deserialization pathway — they can plant a malicious AST that gets executed later.

// ⚠️ DANGEROUS: Loading AST from untrusted/unvalidated storage
const cachedAST = JSON.parse(redis.get('template:welcome')); // Attacker tampered with cache
const compiled = Handlebars.compile(cachedAST); // 💥 Malicious AST executed

Scenario 3: Supply Chain / Dependency Confusion

In complex build pipelines, AST objects might be passed between tools or plugins. A compromised upstream package that produces malicious AST output could trigger RCE in any downstream consumer using a vulnerable Handlebars version.

Real-World Impact

Impact Category Description
Confidentiality Attacker can read files, environment variables, secrets, and database credentials
Integrity Attacker can modify files, plant backdoors, or corrupt data
Availability Attacker can crash the server, delete files, or launch resource exhaustion attacks
Lateral Movement Compromised server can be used to attack internal network resources

This is why this vulnerability is rated CRITICAL. A single successful exploit can lead to complete server compromise.


The Fix

What Changed

The fix was implemented by upgrading Handlebars from version 4.7.8 to 4.7.9. The changes were applied to package.json and pnpm-lock.yaml to ensure the updated version is locked in across all environments.

Before (vulnerable):

// package.json
{
  "dependencies": {
    "handlebars": "4.7.8"
  }
}

After (fixed):

// package.json
{
  "dependencies": {
    "handlebars": "4.7.9"
  }
}

For projects using pnpm, the lock file (pnpm-lock.yaml) was also updated to ensure the resolved package hash points to the patched version:

# pnpm-lock.yaml (simplified)
# Before:
handlebars:
  version: 4.7.8
  resolution: {integrity: sha512-...oldHash...}

# After:
handlebars:
  version: 4.7.9
  resolution: {integrity: sha512-...newHash...}

How the Fix Solves the Problem

Version 4.7.9 introduces stricter validation of AST node properties within the compile() function. Specifically, the patch:

  1. Validates node types against an allowlist — Only recognized, safe AST node types are processed. Unknown or unexpected node structures are rejected before code generation begins.

  2. Sanitizes node property values — Properties that are used in code generation are validated to ensure they cannot contain executable code or escape the template sandbox.

  3. Hardens the code generation phase — The JavaScript code generator applies additional escaping and boundary checks to ensure that even if a malformed node slips through, it cannot produce executable code outside the intended template scope.

The key principle is never trust input, even structured input. An AST object looks like safe, structured data — but its contents can be just as dangerous as raw user input if not properly validated.


Prevention & Best Practices

1. Keep Dependencies Updated

The most effective defense against known vulnerabilities is staying current. Use automated tools to monitor and update dependencies:

# Check for vulnerabilities with npm
npm audit

# Check for vulnerabilities with pnpm
pnpm audit

# Use a dedicated scanner like Trivy
trivy fs --scanners vuln .

# Use Snyk for continuous monitoring
snyk test
snyk monitor

2. Never Compile Untrusted Templates Server-Side

This is the golden rule for Handlebars security:

// ❌ NEVER DO THIS — compiling user-provided templates server-side
app.post('/render', (req, res) => {
  const template = Handlebars.compile(req.body.template);
  res.send(template(req.body.data));
});

// ✅ DO THIS INSTEAD — use pre-approved templates only
const APPROVED_TEMPLATES = {
  welcome: Handlebars.compile('Hello, {{name}}!'),
  invoice: Handlebars.compile('Invoice #{{id}} for {{customer}}'),
};

app.post('/render', (req, res) => {
  const templateName = req.body.templateName;
  if (!APPROVED_TEMPLATES[templateName]) {
    return res.status(400).send('Unknown template');
  }
  res.send(APPROVED_TEMPLATES[templateName](sanitize(req.body.data)));
});

3. Validate and Sanitize AST Objects

If your application must work with AST objects (e.g., for caching), treat them as untrusted input:

// ✅ Validate AST objects before use
function safeCompileAST(ast) {
  // Validate the AST structure against a schema before compiling
  if (!isValidHandlebarsAST(ast)) {
    throw new Error('Invalid AST structure detected');
  }
  return Handlebars.compile(ast);
}

// Use JSON Schema or a dedicated validator for AST validation
const Ajv = require('ajv');
const ajv = new Ajv();
const astSchema = require('./handlebars-ast-schema.json');
const isValidHandlebarsAST = ajv.compile(astSchema);

4. Use Handlebars in "Runtime Only" Mode Where Possible

If you don't need server-side compilation, use the Handlebars runtime, which doesn't include the compiler:

// Use the precompiled runtime — no compile() function available
const Handlebars = require('handlebars/runtime');

// Templates are precompiled at build time, not runtime
// This eliminates the entire attack surface

5. Apply the Principle of Least Privilege

Run your Node.js application with minimal OS permissions. Even if RCE is achieved, a low-privilege process limits what an attacker can do:

# Dockerfile — run as non-root user
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
RUN pnpm install --frozen-lockfile
CMD ["node", "server.js"]

6. Implement Runtime Application Self-Protection (RASP)

Consider tools that can detect and block exploitation attempts at runtime:

  • Sqreen / Datadog ASM — Runtime application security monitoring
  • Node.js --experimental-permission flag — Restrict file system and network access
  • Snyk Runtime — Continuous runtime vulnerability detection

7. Lock Your Dependency Tree

Always commit your lock files (pnpm-lock.yaml, package-lock.json, yarn.lock) and use --frozen-lockfile in CI/CD:

# CI/CD pipeline — use frozen lockfile to prevent unexpected upgrades
pnpm install --frozen-lockfile

# Or with npm
npm ci

Relevant Security Standards


Tooling Recommendations

Tool Purpose How It Helps
Trivy Vulnerability scanning Detected this CVE in pnpm-lock.yaml
Snyk Continuous monitoring Alerts on new CVEs in your dependencies
Dependabot Automated PRs Auto-creates PRs for vulnerable dependencies
Socket.dev Supply chain security Detects malicious packages before install
npm audit Built-in scanning Quick check for known vulnerabilities

Conclusion

CVE-2026-33937 is a stark reminder that templating engines are powerful tools that must be treated with care. The ability to compile templates into executable JavaScript functions is incredibly useful — but it also means that malicious input to the compilation pipeline can have catastrophic consequences.

The key takeaways from this vulnerability are:

  1. Update immediately — Upgrade to Handlebars 4.7.9 if you haven't already. This is a critical fix.
  2. Never compile untrusted input — If users can influence what gets passed to compile(), you have a potential RCE on your hands.
  3. Treat ASTs as untrusted data — Structured data is not inherently safe. Validate it.
  4. Automate your security scanning — This vulnerability was caught by an automated scanner (Trivy). Invest in tooling that continuously monitors your dependency tree.
  5. Defense in depth — Combine dependency updates with least-privilege execution, input validation, and runtime monitoring.

The fact that this fix was identified, verified, and deployed through an automated security pipeline demonstrates exactly how modern security practices should work. Vulnerabilities will always be discovered — what matters is how quickly and reliably you can respond.

Stay secure, keep your dependencies updated, and never stop learning. 🔒


This vulnerability was identified by OrbisAI Security automated scanning and remediated via an automated security fix pipeline. The fix was verified with a Trivy re-scan and LLM-assisted code review.


References

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #13367

Related Articles

critical

Stack Buffer Overflow in MapScale: How Five Unsafe sprintf Calls Created a Critical Vulnerability

A critical stack-based buffer overflow vulnerability was discovered and patched in `src/mapscale.c`, where five unbounded `sprintf` calls wrote formatted output into fixed-size stack buffers without any bounds checking. An attacker controlling unit text strings could overflow the stack buffer, potentially overwriting the function return address and achieving arbitrary code execution. The fix replaces dangerous `sprintf` calls with their bounds-checked counterparts, eliminating the overflow risk

critical

Heap Buffer Overflows in YAML Parser: How Unchecked memcpy Calls Create Critical Attack Vectors

A critical heap buffer overflow vulnerability was discovered and patched in the YAML parser embedded within an Android VPN application, where five unvalidated `memcpy` calls could allow an attacker to corrupt heap memory by supplying a crafted YAML configuration file. This class of vulnerability is particularly dangerous because it can lead to arbitrary code execution or application crashes in security-sensitive contexts. The fix adds proper bounds validation before each copy operation, eliminat

critical

Critical Buffer Overflow Fixed: When "Safe" Functions Aren't Safe

A critical vulnerability in DeepSkyStackerKernel's StackWalker.cpp was silently replacing bounds-checking string functions with their unsafe counterparts via preprocessor macros, exposing the entire codebase to buffer overflow attacks. This fix removes the dangerous macro definitions that discarded buffer size arguments, restoring the intended memory safety protections across all call sites. Understanding how this subtle macro trick works is essential for any C/C++ developer working with string