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

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

medium

Mass Assignment Vulnerability: Why Your Rails Models Need attr_accessible

A medium-severity mass assignment vulnerability was identified in a Ruby on Rails model that lacked proper attribute whitelisting via `attr_accessible` or strong parameters. Without this protection, attackers can manipulate any model attribute through crafted HTTP requests, potentially escalating privileges or corrupting data. The fix enforces explicit attribute allowlisting, closing the door on unauthorized mass assignment exploitation.

critical

Shell Injection via os.system(): How a Single Line of Code Can Compromise Your System

A critical OS command injection vulnerability (CWE-78) was discovered and patched in `voice.py`, where user-controlled input was interpolated directly into a shell command string passed to `os.system()`. An attacker who could influence the `device` variable — through a config file, environment variable, or any external input — could execute arbitrary system commands with the full privileges of the running process. The fix replaces the dangerous `os.system()` calls with Python's `subprocess.run()

critical

Command Injection via os.system() in DeepSpeed's Data Analyzer: A Critical Fix

A critical command injection vulnerability was discovered in DeepSpeed's `data_analyzer.py`, where an `os.system()` call directly interpolated an unsanitized file path variable into a shell command string. An attacker who could influence dataset configuration or file paths could execute arbitrary shell commands on the host machine. The fix replaces the dangerous shell invocation with safe, Python-native file operations that never touch a shell interpreter.

high

CVE-2026-40073: How a BODY_SIZE_LIMIT Bypass in @sveltejs/adapter-node Put Your App at Risk

CVE-2026-40073 is a high-severity vulnerability in `@sveltejs/adapter-node` that allows attackers to bypass the `BODY_SIZE_LIMIT` configuration, potentially enabling denial-of-service attacks and resource exhaustion against SvelteKit applications. The vulnerability was silently present in versions prior to `@sveltejs/kit` 2.57.1, and has now been patched by upgrading the dependency across all affected project examples. If your application relies on body size limits to protect against oversized p

medium

From eval() to ast.literal_eval(): Closing a Code Injection Door in Slack Data Processing

A medium-severity vulnerability was discovered in a Slack data processing component where the use of Python's built-in `eval()` function to parse error message dictionaries could allow an attacker to inject and execute arbitrary code. The fix replaces `eval()` with the safer `ast.literal_eval()`, which safely evaluates only Python literals without executing arbitrary expressions. This change eliminates a critical attack surface that could have been exploited through crafted error messages return

critical

Critical Buffer Overflow in ELF Parser: How a Missing Bounds Check Almost Became a Heap Exploit

A critical out-of-bounds memory vulnerability was discovered and patched in `utils/symbol-rawelf.c`, where two separate `memcpy` calls lacked proper bounds validation when processing ELF binary files. Without these checks, a maliciously crafted ELF file could trigger an out-of-bounds read or heap overflow, potentially leading to remote code execution or memory corruption. This post breaks down how the vulnerability works, how it was fixed, and what every C developer should know about safe memory