Critical n8n flaw CVE-2026-25049: remote code execution via sandbox escape

Analysis of CVE-2026-25049, a critical CVSS 9.4 flaw in n8n enabling RCE through a sandbox escape. Affected versions, technical exploit and hardening.

On February 4, 2026, the DevOps community was shaken by the publication of CVE Apple dyld-2026-25049, a critical vulnerability affecting n8n, one of the most popular workflow automation tools on the market. With a CVSS score of 9.4, this flaw lets an attacker run arbitrary system commands on the server hosting n8n simply by sending an HTTP request to a public webhook. No authentication required.

Every version prior to 1.123.17 and 2.5.2 is affected. And the problem is all the more serious because n8n is massively deployed by DevOps teams, startups and SMEs that use it to orchestrate hundreds of business workflows wired to sensitive APIs.

Here is a technical breakdown of this flaw, how it is actually exploited, and the steps to take immediately to secure your instances.

Immediate action required: If you run n8n in production, upgrade to version 2.5.2 or 1.123.17 at minimum. Earlier versions allow unauthenticated remote code execution through a single webhook.

n8n: a quick reminder about the tool and its ecosystem

Before diving into the vulnerability, a quick reminder is in order for those who don't know n8n yet. It is an open source workflow automation platform, often compared to Zapier or Make but self-hostable. The tool lets you build visual automation flows by connecting hundreds of services together: databases, REST APIs, messaging platforms, monitoring tools, cloud services.

n8n has carved out a place in the DevOps ecosystem for several reasons:

  • Self-hosting: unlike Zapier, you keep full control over your data and your infrastructure
  • Extensibility: more than 400 native integrations, plus the ability to build your own nodes in JavaScript
  • Dynamic expressions: a powerful expression system that lets you manipulate data between the steps of a workflow
  • Webhooks: the ability to trigger workflows through publicly exposed HTTP endpoints

It is precisely this combination of dynamic expressions and public webhooks that forms the attack vector for CVE-2026-25049. No-code and low-code tools like n8n deliver remarkable productivity, but they also introduce attack surfaces that many teams underestimate.

The technical vulnerability: sandbox escape via type confusion

n8n's expression engine lets users insert dynamic JavaScript into workflows, between double curly braces: {{ expression }}. To prevent that arbitrary JavaScript from compromising the server, n8n uses an execution sandbox that restricts access to dangerous objects and properties.

The core security mechanism relies on a filtering function called isSafeObjectProperty():

// Sanitization function in n8n (vulnerable version)
export function isSafeObjectProperty(property: string) {
    return !unsafeObjectProperties.has(property);
}

// The blacklist blocks dangerous properties
const unsafeObjectProperties = new Set([
    '__proto__',
    'constructor',
    'prototype',
    'toString',
    // ... other sensitive properties
]);

In theory, this approach looks solid: any attempt to access __proto__ or constructor is blocked. But the problem lies in a fundamental mismatch between TypeScript and JavaScript.

The TypeScript vs JavaScript trap

The TypeScript signature of the function declares that the property parameter is of type string. At compile time, TypeScript guarantees that only strings can be passed. But TypeScript exists only at compile time. At runtime, what runs is plain JavaScript, and JavaScript has no type system at execution time.

An attacker can therefore pass an array instead of a string. The Set.has() method uses a strict comparison (===), which means ["__proto__"] (an array) will never be equal to "__proto__" (a string). The filter is bypassed.

// Demonstrating the bypass
// The blacklist blocks the string "__proto__"
unsafeObjectProperties.has("__proto__");      // true - blocked
unsafeObjectProperties.has(["__proto__"]);    // false - bypassed!

// JavaScript then converts the array to a string
// when accessing the object property
const obj = {};
obj[["__proto__"]];  // Equivalent to obj["__proto__"] at runtime

This is the key to the exploit: JavaScript performs an implicit type coercion when accessing an object's properties. The array ["__proto__"] is automatically converted to the string "__proto__" by the JavaScript engine. The filter is bypassed, yet access to the dangerous property works perfectly.

From theory to exploitation: RCE in a single request

By combining this type confusion with JavaScript's bracket notation, an attacker can craft expressions that escape the sandbox entirely and reach Node.js system primitives.

Step 1: prototype pollution

The first step is to confirm the bypass works by accessing __proto__ through an array:

// n8n expression injected through a webhook
{{ {}[["__proto__"]].polluted = 23 }}

// The sandbox does not block ["__proto__"] (array)
// but JavaScript resolves the access as __proto__ (string)

Step 2: reaching the Function constructor

The attacker then walks up the prototype chain to reach the Function constructor, which allows arbitrary JavaScript code to be executed:

// Reaching the Function constructor through toString
{{ {}[["toString"]][["constructor"]]("p","return p.env")(process) }}

// Breakdown:
// 1. {}[["toString"]]       → accesses Object.prototype.toString
// 2. [["constructor"]]      → accesses Function (constructor of toString)
// 3. ("p","return p.env")   → creates a new function
// 4. (process)              → runs it with Node.js's process object

Step 3: running system commands

Once access to the process object is obtained, the attacker uses process.binding('spawn_sync') to directly invoke the system process-creation functions, bypassing the restrictions of Node.js's child_process module:

# The attacker simply sends an HTTP request to the webhook
curl -X POST https://n8n-vulnerable.example.com/webhook/workflow-id \
  -H "Content-Type: application/json" \
  -d '{"payload": "malicious expression"}'

# The n8n server runs the command with the privileges
# of the n8n process (often root in Docker deployments)

The result is full command execution on the server. The attacker can read files, exfiltrate environment variables containing credentials, install backdoors or pivot to other systems on the network.

A realistic attack scenario

To fully grasp how serious this flaw is, here is a typical exploitation scenario as it could unfold in a company running n8n:

  1. Reconnaissance: the attacker scans the Internet for exposed n8n instances (default port 5678, or detection via HTTP headers)
  2. Identifying webhooks: n8n webhook URLs follow a predictable pattern, and some workflows use webhooks without authentication
  3. Injecting the expression: a single POST request to the webhook with the malicious expression in the JSON body
  4. Exfiltration: retrieving environment variables (process.env) containing API keys, database tokens and credentials for connected services
  5. Persistence: installing a backdoor (reverse shell, SSH key, crontab) to maintain access
  6. Lateral movement: using the exfiltrated credentials to reach databases, cloud services and third-party APIs connected to n8n

The danger is amplified by the fact that n8n stores the credentials of every connected service (databases, third-party APIs, cloud services) directly in its configuration. Compromising n8n potentially means compromising the entire ecosystem of tools it is connected to.

Checking whether your instance is vulnerable

The first step is to determine which version of n8n you are running. Several methods, depending on your deployment type:

# Method 1: via the n8n CLI
n8n --version

# Method 2: via the API (if accessible)
curl -s https://your-n8n-instance.com/api/v1/version

# Method 3: via Docker
docker exec n8n n8n --version

# Method 4: check the Docker image in use
docker inspect n8n | grep -i image

# Vulnerable versions:
# - Any version < 1.123.17 (1.x branch)
# - Any version < 2.5.2 (2.x branch)

Next, check whether any webhooks are exposed without authentication:

# List active workflows with webhooks
# In the n8n interface, check:
# 1. Workflows > filter by "Webhook" trigger
# 2. For each workflow, check the "Authentication" field
# 3. If "None" → the webhook is reachable without auth

# Check from the outside whether the n8n port is exposed
nmap -p 5678 your-server.com

# Test the reachability of a known webhook
curl -I https://your-n8n-instance.com/webhook/test

If your version is below the thresholds listed and webhooks are exposed, your instance is actively exploitable from the Internet.

Fixing and hardening: immediate action plan

1. Upgrade n8n

The absolute priority is the upgrade. The fix shipped in versions 1.123.17 and 2.5.2 adds runtime type validation:

// Fix applied in n8n 2.5.2
export function isSafeObjectProperty(property: string) {
    // Runtime validation added - no longer trusts the TypeScript type
    if (typeof property !== 'string') {
        return false;  // Reject anything that is not a string
    }
    return !unsafeObjectProperties.has(property);
}

To upgrade depending on your deployment method:

# Upgrade via npm
npm update -g n8n

# Upgrade via Docker Compose
# In docker-compose.yml, change the image:
# image: n8nio/n8n:2.5.2
docker compose pull && docker compose up -d

# Upgrade via standalone Docker
docker pull n8nio/n8n:2.5.2
docker stop n8n
docker rm n8n
docker run -d --name n8n \
  -p 5678:5678 \
  -v n8n_data:/home/node/.n8n \
  n8nio/n8n:2.5.2

# Post-upgrade verification
n8n --version
# Expected: 2.5.2 or higher

2. Place n8n behind a reverse proxy with authentication

Even after the upgrade, n8n should never be exposed directly on the Internet. Place it behind an Nginx reverse proxy with an additional layer of authentication:

# Nginx configuration for n8n
server {
    listen 443 ssl http2;
    server_name n8n.your-domain.com;

    ssl_certificate /etc/letsencrypt/live/n8n.your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/n8n.your-domain.com/privkey.pem;

    # Basic authentication on the admin interface
    location / {
        auth_basic "n8n Admin";
        auth_basic_user_file /etc/nginx/.htpasswd-n8n;

        proxy_pass http://127.0.0.1:5678;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket support (required for the n8n editor)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Webhooks: restrict by IP or add rate limiting
    location /webhook/ {
        # Option A: restrict to known IPs
        # allow 203.0.113.0/24;
        # deny all;

        # Option B: aggressive rate limiting
        limit_req zone=webhook burst=10 nodelay;

        proxy_pass http://127.0.0.1:5678;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

To go deeper into production Nginx configuration, see the article Nginx in production: 7 optimizations that change everything.

3. Isolate n8n at the network level

n8n should only have access to the services it genuinely needs. In a Docker environment, use dedicated networks:

# Hardened docker-compose.yml for n8n
version: '3.8'

services:
  n8n:
    image: n8nio/n8n:2.5.2
    restart: unless-stopped
    environment:
      - N8N_HOST=n8n.your-domain.com
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://n8n.your-domain.com/
      # Disable public sign-up
      - N8N_USER_MANAGEMENT_DISABLED=false
      # Force authentication
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=${N8N_USER}
      - N8N_BASIC_AUTH_PASSWORD=${N8N_PASSWORD}
    volumes:
      - n8n_data:/home/node/.n8n
    networks:
      - n8n-internal
      - proxy-network
    # Do NOT expose the port directly
    # ports:
    #   - "5678:5678"
    # Limit capabilities
    security_opt:
      - no-new-privileges:true
    # Non-root user
    user: "1000:1000"
    # Limit resources
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G

networks:
  n8n-internal:
    internal: true  # No direct Internet access
  proxy-network:
    external: true  # Network shared with Nginx

For Docker best practices in production, particularly handling non-root users and capabilities, see Docker in production: the mistakes I see most often.

4. Audit existing workflows

After the upgrade, it is essential to verify that your workflows have not already been compromised:

# Search for suspicious expressions in exported workflows
# Export all workflows via the API
curl -s -H "X-N8N-API-KEY: your-api-key" \
  https://n8n.your-domain.com/api/v1/workflows | \
  python3 -c "
import sys, json
data = json.load(sys.stdin)
keywords = ['__proto__', 'constructor', 'spawn_sync', 'binding', 'toString']
for wf in data.get('data', []):
    wf_str = json.dumps(wf)
    for kw in keywords:
        if kw in wf_str:
            print(f\"SUSPECT: Workflow '{wf[\"name\"]}' contains '{kw}')
"

# Check the logs for exploitation attempts
grep -rE "(spawn_sync|__proto__|constructor.*constructor)" /var/log/n8n/
journalctl -u n8n --since "2026-02-01" | grep -iE "error|exploit|spawn"

Lessons for DevOps teams

This CVE goes beyond the specific case of n8n. It highlights structural problems found in many automation tools deployed in production.

The "it's just an internal tool" trap

Many teams deploy tools like n8n, GitLab CI, Jenkins or Airflow believing they are internal tools that don't need the same level of hardening as a public-facing application. This is a fundamental mistake. These tools:

  • Store sensitive credentials (API tokens, database passwords, SSH keys)
  • Hold elevated privileges to interact with the infrastructure
  • Often expose public endpoints (webhooks, APIs) even when the rest of the interface is protected
  • Are rarely updated with the same rigor as the core business applications

Never trust type annotations for security

The main technical lesson from this CVE is clear: TypeScript types are not a security control. TypeScript is a development tool that improves code quality at compile time, but it disappears completely at runtime. Any security validation must include explicit runtime checks:

// BAD: trusts the TypeScript type
function sanitize(input: string): boolean {
    return !blacklist.has(input);
}

// GOOD: explicit runtime validation
function sanitize(input: string): boolean {
    if (typeof input !== 'string') {
        return false;  // Reject unexpected types immediately
    }
    return !blacklist.has(input);
}

This pattern applies to any code that handles user input, whether in TypeScript, Python with type hints, or any other language with an optional type system.

Network isolation is non-negotiable

Defense in depth is not optional. Even if n8n had never had this vulnerability, an automation tool with access to sensitive credentials must be:

  • Isolated in a dedicated network with no direct Internet access (except for the services explicitly required)
  • Placed behind a reverse proxy with authentication and rate limiting
  • Actively monitored with alerts on abnormal behavior (unusual outbound connections, access to system files)
  • Updated regularly with a dedicated CVE watch process

To set up an effective firewall on the server hosting your automation tools, the UFW tutorial covers the basics, and iptables gives you finer control for advanced configurations.

Hardening checklist for automation tools

In addition to the n8n-specific fix, here is a checklist applicable to any automation tool deployed in production:

# Security checklist - Automation tools
# ==============================================

# 1. Network exposure
nmap -p- your-server.com            # Check open ports
ss -tlnp | grep -E "5678|8080"     # The tool's listening ports

# 2. Run-as user
ps aux | grep n8n                   # Must NOT be root
id $(ps -o user= -p $(pgrep n8n))  # Check the user

# 3. Docker isolation
docker inspect n8n | grep -A5 Networks  # Attached networks
docker inspect n8n | grep Privileged     # Must be false

# 4. Stored credentials
# Make sure secrets live in env variables
# and are NOT hardcoded in config files
grep -rn "password\|token\|key" /home/node/.n8n/

# 5. Logs and monitoring
# Make sure logs are centralized and alerting
journalctl -u n8n --since "1 hour ago" | tail -20

For a more complete security checklist applicable to all your Linux servers, see Security checklist: 10 things to check on any Linux server.

Context: n8n under security pressure

CVE-2026-25049 is not an isolated incident. On February 4, 2026, ten additional CVEs were published simultaneously for n8n, revealing a far broader attack surface than many administrators imagined. This burst of vulnerabilities echoes a similar episode in late 2025, when a CVSS 9.9 flaw had already allowed arbitrary code execution on thousands of exposed instances.

The pattern recurs in fast-growing open source tools: prioritizing features and adoption builds up security debt that eventually materializes as critical CVEs. This is not unique to n8n; the same dynamic has been seen in the past with Jenkins, Grafana or GitLab.

For teams that depend on these tools, the strategy is clear: never assume an automation tool is safe by default, regardless of its popularity or its community. Security is built in layers, and trust must be verified regularly.

Conclusion

CVE-2026-25049 is a perfect illustration of why the security of internal DevOps tools deserves as much attention as that of public-facing applications. An automation tool like n8n, by its very nature, concentrates privileged access to many services. A single flaw in its expression sandbox, and the entire connected infrastructure becomes accessible to an attacker.

The takeaways:

  • Upgrade immediately to n8n 2.5.2 or 1.123.17
  • Audit your webhooks and remove those that don't need public access
  • Isolate n8n in a dedicated network behind an authenticated reverse proxy
  • Monitor expressions and logs to detect any exploitation attempt
  • Apply the principle of least privilege: n8n should only have access to the services strictly necessary

And more broadly, this CVE is a reminder that compile-time type validation does not replace runtime security checks. Whatever the language, whatever the framework, user input must be validated explicitly at every layer of the application.

Did you enjoy this article?

Comments

Morgann Riu

Cybersecurity and Linux administration expert. I help companies secure and optimize their critical infrastructures.

Back to the blog

Checklist Sécurité Linux

30 points essentiels pour sécuriser un serveur Linux. Recevez aussi les nouveaux tutoriels par email.

Pas de spam. Désabonnement en 1 clic.