Skip to main content

Three JavaScript Quirks, One CVSS 10.0 RCE: What n8n's Sandbox-Escape Chain Means for Every Agent Tool Wired to Your Cluster

9 min readDora NodaDora Noda
Share
On this page

Three JavaScript syntax quirks — a backtick instead of a bracket, an error's stack trace, and an arrow function's inherited this — were all it took to turn n8n's workflow sandbox into a root shell. Not three separate bugs shipped in three separate advisories. Three gaps in the same sanitizer, chained together by Pillar Security into CVE-2026-25049, a CVSS 10.0 remote code execution vulnerability that let any authenticated workflow author decrypt every credential n8n had ever stored and, on shared instances, reach into the Kubernetes cluster running underneath it.

If your team wires an "agent tool" — human-facing or AI-facing — into infrastructure that matters, this is the disclosure to read closely. Not because n8n is uniquely careless. Because the shape of the bug is generic: any tool that hands its caller a general-purpose scripting sandbox is one syntax edge case away from the same outcome.

The chain, step by step

n8n lets a workflow author drop JavaScript expressions into {{ }} templates — read a field, transform a value, branch on a condition. To keep that from becoming an open code-execution endpoint, n8n runs those expressions through a PrototypeSanitizer that denylists dangerous property access before the expression ever reaches V8. Pillar Security found three independent gaps in that denylist, and — critically — none of them was fatal alone. Chained together, they were.

Gap one: the denylist checked the wrong syntax. The sanitizer blocked bracket and dot-notation access to sensitive properties — Error['prepareStackTrace'], obj.constructor — by pattern-matching those access forms directly. It never accounted for backtick template-literal property access, Error[`prepareStackTrace`], which reaches the identical property through different syntax. Same destination, unguarded road.

Gap two: that property is a hook into V8 internals. Error.prepareStackTrace is a real, documented V8 API — set it to a function, and V8 calls it with an array of CallSite objects any time an error's stack gets read. One of those CallSite methods, getThis(), returns the actual execution context an error occurred in. Trigger an error inside the sandboxed expression after setting a malicious prepareStackTrace, and getThis() hands back the real global object — the one the sandbox was supposed to keep out of reach entirely.

Gap three: the this-neutralization had a blind spot. n8n's sanitizer also strips dangerous this-context from ordinary function expressions, specifically so a compromised global object can't be smuggled back in through a function's own this. But ArrowFunctionExpression nodes don't have their own this — they inherit it lexically from the enclosing scope by design, and the sanitizer never checked for that node type. Wrap the escape in an arrow function, and it inherits this bound to the now-compromised global object the previous two steps exposed, with nothing left to strip.

Three gaps, three checks that each individually looked reasonable, and a full escape once chained: arbitrary command execution on the n8n server, as the n8n process's own user. n8n shipped an incomplete fix on December 23, 2025 that closed only the template-literal path; the complete fix landed in v2.4.0 in January 2026, after Pillar's full chain forced the deeper architectural review. Every instance running an older version was exploitable by anyone who could create or edit a workflow — no admin privileges required, just the standard permission n8n gives any workflow author by default.

What "every stored credential and the Kubernetes cluster underneath" actually means

The title of this post makes two distinct claims, and it's worth keeping them separate, because they land on different deployments differently.

The credential claim is universal. Once an attacker has arbitrary command execution as the n8n process, they can read N8N_ENCRYPTION_KEY straight out of the environment — the key n8n uses to encrypt every credential a workflow ever referenced. That's not a narrow set of secrets. Pillar's writeup lists AI API keys, cloud provider keys (AWS, Azure), database passwords, and OAuth tokens as the concrete categories recovered in their proof of concept — anything an organization ever wired into a workflow becomes plaintext the moment the encryption key does. This applies to any vulnerable self-hosted instance, full stop.

The cluster claim is scoped, and the scoping matters. On n8n Cloud's shared, multi-tenant Kubernetes environment specifically, the same RCE foothold reached further than the local container: internal services including secrets-api, hooks-api, and an internal npm registry, with researchers noting the same access could plausibly pivot toward other tenants' workflows and data on shared infrastructure. That's a materially different blast radius than a single self-hosted instance getting popped — it's the multi-tenant version of the same bug, where the "cluster" in the title isn't a figure of speech but the literal orchestration layer the vulnerable process was running inside of.

The pattern underneath both claims is the same one that shows up in every "single compromised service reaches way more than it should" incident: a workload got a service account, a set of secrets, and network reachability sized for its worst-case need rather than its typical one, and a bug in the workload was all it took to spend that entire budget at once.

This isn't an n8n problem — it's a category

n8n gets scrutiny here because Pillar's disclosure is unusually well-documented, not because n8n is an outlier. The category is: any tool that hands its caller a general-purpose scripting sandbox — to a human workflow author today, increasingly to an AI agent's tool-calling loop tomorrow — inherits the job of getting that sandbox's denylist exhaustively right, forever, against a language with as many equivalent-but-differently-spelled ways to reach the same property as JavaScript has. A .constructor check that doesn't also catch ["constructor"] and [`constructor`] isn't three-quarters of a fix. It's a fix with two open doors next to the one that's locked.

That framing matters more, not less, as the caller stops being a human. Teleport's 2026 Infrastructure Identity Survey found 70% of organizations already grant their AI systems more access than a human filling the equivalent role — the access-provisioning habits built for people are being copy-pasted onto agents faster than anyone is re-deriving what those agents actually need. CNCF's agentic-infrastructure guidance and Microsoft's own 2026 least-privilege recommendations converge on the same countermeasures, and they're worth naming concretely rather than gesturing at "be careful":

  • A unique, lifecycle-managed identity per agent instance — a SPIFFE ID or a dedicated Kubernetes service account, not a shared credential every workflow or agent run reuses.
  • Short-lived, scoped tokens requested just-in-time, not a long-lived credential sitting in an environment variable waiting for exactly the kind of RCE this post just walked through to read it.
  • Default-deny network policy, so a compromised workload's blast radius is bounded by what it was explicitly allowed to reach, not by what happened to be reachable on the pod's network.
  • A scoped tool manifest — the agent (or workflow) can call the specific operations it needs, and nothing generatively broader than that.

None of these prevent a sandbox bug from existing. What they do is cap what that bug is worth once it fires — the difference between an RCE that reads one workflow's credentials and an RCE that reads every credential the organization ever stored plus a foothold in the cluster.

The architectural alternative: don't expose the sandbox at all

There's a version of this fix that isn't "write a better sanitizer." It's "don't hand the caller a general-purpose interpreter for a job that doesn't need one."

n8n's attack surface here exists because the product's core value proposition — a workflow author drops in {{ any.js.expression }} and it just runs — requires evaluating arbitrary caller-supplied JavaScript in the request path. Once "evaluate arbitrary code" is the actual product, keeping that sandbox airtight against every syntactically-equivalent way to reach a denylisted property becomes an unbounded, adversarial job that never finishes — this disclosure closed three gaps knowing more probably exist, because the surface being defended is Turing-complete.

A deploy-from-git platform's agent-facing surface can look structurally different. When an AI agent — or a human — needs to deploy a service, roll back a release, or scale a fleet, a Render-compatible API or an MCP server can expose exactly those verbs as fixed, typed operations, with no expression language sitting in between for an attacker to escape from. There's no PrototypeSanitizer to bypass because there's no general-purpose interpreter evaluating caller-supplied code in the first place. That's not a claim that a typed API can't have its own bugs — scoped tokens still need to be scoped correctly, and an authorization check on a deploy call still has to be enforced — but it does mean this specific vulnerability class, a sandbox escape reached by finding the syntax the denylist forgot, has nothing to escape from.

Bex.co is the open-source, AI-native Render alternative — deploy-from-git with a Render-compatible API and an MCP server scoped to fixed operations like deploy, rollback, and scale, running on machines you own rather than a shared multi-tenant control plane. Star the repo on GitHub or deploy your first app today.

The lesson survives the next CVE

n8n will patch this chain, and Pillar (or someone else) will eventually find the next one — the incomplete December fix that only closed the template-literal path is a reminder that a sandbox's denylist is never provably complete, just complete against the inputs someone has thought to try so far. That's true of every general-purpose sandbox, not a knock specific to n8n's implementation.

The durable takeaway isn't "audit your JavaScript sanitizer harder." It's that every tool wired into infrastructure that matters is making a choice, whether or not anyone states it out loud: expose a general-purpose execution surface and commit to defending it forever, or expose a fixed set of operations and remove the sandbox-escape question from the table entirely. As more of that wiring routes through AI agents calling tools rather than humans clicking through a workflow builder, the second choice stops being a nice-to-have architectural preference and starts being the difference between a contained incident and a full credential vault walking out the door with the cluster's service account behind it.

Sources:

Related articles

Give your agents a chain backend

Autonomous agents hit RPC endpoints very differently than people do. See what bex router handles on their behalf.

Read the agents guide